mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ac174fdc6 | |||
| 4bf9b12680 | |||
| 2028cee274 | |||
| 1c22c0f01c | |||
| 5e28721651 | |||
| 95547ad7c7 | |||
| 3fd96834f9 | |||
| 3da05eea02 | |||
| 1e788e46f5 | |||
| e0d42fe6d3 | |||
| 28f57525f4 | |||
| 62496f78a8 | |||
| 36ba4f34bb | |||
| 691573fbe5 | |||
| 7f623c0c1f | |||
| ad93cbba7a | |||
| 756fc431b3 | |||
| 71ce855feb | |||
| 5705e39f53 | |||
| 27c9c8cda7 | |||
| ad46f8aca0 | |||
| cad9328e5d | |||
| ac875a328e | |||
| 8ad923a90a | |||
| b43979bbba | |||
| 1a56b37e4e | |||
| 4340828fec | |||
| f74b6476de | |||
| 3011735458 | |||
| d40aac8960 | |||
| 18672f541e | |||
| 58aed76bbb | |||
| 77ec805931 | |||
| 505db703d8 | |||
| cc19b626e1 | |||
| fc157dfd79 | |||
| f407ef6a0c | |||
| 248ca1d522 | |||
| d41f1b9495 | |||
| 6efab80686 | |||
| 6253ca46bc | |||
| 1f10e96288 | |||
| ae69b138bf | |||
| b2dd9a6e13 | |||
| 9bc66798d4 | |||
| 114f1a7c24 | |||
| 14cc7ef2ae | |||
| 7183df6938 | |||
| b862fc4113 | |||
| 22038533d7 | |||
| 5641cadeec | |||
| 4eacdd543a | |||
| a0221bb79c | |||
| 82f0db107a | |||
| d2ce0918b5 | |||
| 8f4fce1219 | |||
| b7ecf1dfcf | |||
| e968b8ed9c | |||
| 96627e964f | |||
| 6e0ff557a8 | |||
| f4790e56ea | |||
| 760b37449e | |||
| fb2a7a6676 | |||
| a89c2edb99 | |||
| 9bf33d2bae |
+15
-1
@@ -1,15 +1,29 @@
|
||||
workflow:
|
||||
rules:
|
||||
# MR pipeline
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
when: always
|
||||
|
||||
# Push pipeline hanya untuk env branch
|
||||
- if: '$CI_COMMIT_BRANCH == "development"'
|
||||
when: always
|
||||
- if: '$CI_COMMIT_BRANCH == "staging"'
|
||||
when: always
|
||||
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||
when: always
|
||||
|
||||
# Selain itu jangan buat pipeline
|
||||
- when: never
|
||||
|
||||
include:
|
||||
- local: "ci/development.yml"
|
||||
# khusus MR (notif)
|
||||
- local: "ci/merge_request.yml"
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
|
||||
# khusus push ke branch env
|
||||
- local: "ci/development.yml"
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "development"'
|
||||
|
||||
- local: "ci/staging.yml"
|
||||
|
||||
+6
-5
@@ -4,9 +4,14 @@ stages:
|
||||
deploy-dev:
|
||||
stage: deploy
|
||||
image: alpine:3.20
|
||||
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "development"'
|
||||
when: on_success
|
||||
- when: never
|
||||
|
||||
variables:
|
||||
DEPLOY_APP: "LTI-MBUGROUP"
|
||||
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
GIT_DEPTH: "1"
|
||||
|
||||
@@ -27,7 +32,6 @@ deploy-dev:
|
||||
|
||||
script:
|
||||
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
|
||||
|
||||
- >
|
||||
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
|
||||
set -e
|
||||
@@ -83,8 +87,5 @@ deploy-dev:
|
||||
curl -sS -H "Content-Type: application/json" \
|
||||
-d @payload.json "$DISCORD_WEBHOOK_URL";
|
||||
|
||||
only:
|
||||
- development
|
||||
|
||||
environment:
|
||||
name: development
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
stages:
|
||||
- notify
|
||||
|
||||
notify_discord_on_mr_request_main_dev:
|
||||
stage: notify
|
||||
image: alpine:3.20
|
||||
rules:
|
||||
# hanya MR yang target ke main atau development
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" && ($CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main" || $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "development")'
|
||||
when: on_success
|
||||
- when: never
|
||||
|
||||
script:
|
||||
- apk add --no-cache curl jq coreutils
|
||||
- |
|
||||
TIME_HUMAN="$(date '+%d/%m/%y, %H.%M')"
|
||||
TIME_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
TITLE="${CI_MERGE_REQUEST_TITLE}"
|
||||
IID="!${CI_MERGE_REQUEST_IID}"
|
||||
USER_LINE="${GITLAB_USER_NAME} (${GITLAB_USER_LOGIN})"
|
||||
PROJECT_PATH="${CI_PROJECT_PATH}"
|
||||
USERNAME="${GITLAB_USER_LOGIN}"
|
||||
MR_URL="${CI_PROJECT_URL}/-/merge_requests/${CI_MERGE_REQUEST_IID}"
|
||||
|
||||
DESC="$(printf "**%s**\n\n%s opened merge request %s %s\n%s" \
|
||||
"$USERNAME" "$USER_LINE" "$IID" "$TITLE" "$TIME_HUMAN")"
|
||||
|
||||
payload=$(jq -n \
|
||||
--arg desc "$DESC" \
|
||||
--arg project "$PROJECT_PATH" \
|
||||
--arg timeiso "$TIME_ISO" \
|
||||
--arg mrurl "$MR_URL" \
|
||||
'{
|
||||
"username": "Mock-api - Merge Requests",
|
||||
"embeds": [
|
||||
{
|
||||
"description": ($desc + "\n" + $mrurl),
|
||||
"color": 15105570,
|
||||
"footer": { "text": $project },
|
||||
"timestamp": $timeiso
|
||||
}
|
||||
]
|
||||
}')
|
||||
|
||||
curl -sS -H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"$DISCORD_WEBHOOK_URL"
|
||||
+12
-27
@@ -8,12 +8,6 @@ default:
|
||||
tags:
|
||||
- self-hosted-prod
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||
when: always
|
||||
- when: never
|
||||
|
||||
variables:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
|
||||
@@ -30,7 +24,9 @@ variables:
|
||||
build_production:
|
||||
stage: build
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||
when: on_success
|
||||
- when: never
|
||||
script: |
|
||||
set -e
|
||||
docker info
|
||||
@@ -47,14 +43,15 @@ build_production:
|
||||
docker tag "$IMAGE_NAME" "$IMAGE_LATEST"
|
||||
docker push "$IMAGE_LATEST"
|
||||
|
||||
|
||||
# =========================
|
||||
# MIGRATE (PRODUCTION)
|
||||
# =========================
|
||||
migrate_production:
|
||||
stage: migrate
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||
when: on_success
|
||||
- when: never
|
||||
needs:
|
||||
- job: build_production
|
||||
artifacts: false
|
||||
@@ -66,12 +63,10 @@ migrate_production:
|
||||
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)
|
||||
@@ -81,21 +76,13 @@ migrate_production:
|
||||
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 ..."
|
||||
# NOTE: pastikan nama servicenya benar untuk production (ini sebelumnya masih stg-*)
|
||||
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"
|
||||
|
||||
@@ -111,7 +98,6 @@ migrate_production:
|
||||
|
||||
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
|
||||
@@ -124,17 +110,16 @@ migrate_production:
|
||||
|
||||
echo "✅ Migration applied successfully"
|
||||
|
||||
|
||||
# =========================
|
||||
# DEPLOY (AUTO)
|
||||
# =========================
|
||||
deploy_production:
|
||||
stage: deploy
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"'
|
||||
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||
when: on_success
|
||||
- when: never
|
||||
needs:
|
||||
# - job: migrate_production
|
||||
# artifacts: false
|
||||
- job: build_production
|
||||
artifacts: false
|
||||
script: |
|
||||
@@ -150,7 +135,6 @@ deploy_production:
|
||||
docker compose -f "$COMPOSE_FILE" up -d --force-recreate
|
||||
docker image prune -f
|
||||
|
||||
|
||||
# =========================
|
||||
# SEED (MANUAL)
|
||||
# =========================
|
||||
@@ -159,9 +143,10 @@ seed_production:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "production"'
|
||||
when: manual
|
||||
- when: never
|
||||
script: |
|
||||
set -e
|
||||
cd /opt/deploy/lti
|
||||
cd "$DEPLOY_DIR"
|
||||
test -f .env || (echo "❌ .env not found" && exit 1)
|
||||
|
||||
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||
|
||||
+13
-22
@@ -8,12 +8,6 @@ default:
|
||||
tags:
|
||||
- self-hosted-stg
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||
when: always
|
||||
- when: never
|
||||
|
||||
variables:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
|
||||
@@ -30,7 +24,9 @@ variables:
|
||||
build_staging:
|
||||
stage: build
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||
- if: '$CI_COMMIT_BRANCH == "staging"'
|
||||
when: on_success
|
||||
- when: never
|
||||
script: |
|
||||
set -e
|
||||
docker info
|
||||
@@ -47,14 +43,15 @@ build_staging:
|
||||
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"'
|
||||
- if: '$CI_COMMIT_BRANCH == "staging"'
|
||||
when: on_success
|
||||
- when: never
|
||||
needs:
|
||||
- job: build_staging
|
||||
artifacts: false
|
||||
@@ -66,12 +63,10 @@ migrate_staging:
|
||||
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)
|
||||
@@ -81,21 +76,17 @@ migrate_staging:
|
||||
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"
|
||||
|
||||
@@ -111,7 +102,6 @@ migrate_staging:
|
||||
|
||||
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
|
||||
@@ -124,14 +114,15 @@ migrate_staging:
|
||||
|
||||
echo "✅ Migration applied successfully"
|
||||
|
||||
|
||||
# =========================
|
||||
# DEPLOY (AUTO)
|
||||
# =========================
|
||||
deploy_staging:
|
||||
stage: deploy
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||
- if: '$CI_COMMIT_BRANCH == "staging"'
|
||||
when: on_success
|
||||
- when: never
|
||||
needs:
|
||||
- job: migrate_staging
|
||||
artifacts: false
|
||||
@@ -150,18 +141,18 @@ deploy_staging:
|
||||
docker compose -f "$COMPOSE_FILE" up -d --force-recreate
|
||||
docker image prune -f
|
||||
|
||||
|
||||
# =========================
|
||||
# SEED (MANUAL)
|
||||
# =========================
|
||||
seed_staging:
|
||||
stage: seed
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||
- if: '$CI_COMMIT_BRANCH == "staging"'
|
||||
when: manual
|
||||
- when: never
|
||||
needs:
|
||||
- job: deploy_staging
|
||||
artifacts: false
|
||||
when: manual
|
||||
allow_failure: false
|
||||
script: |
|
||||
set -e
|
||||
@@ -170,4 +161,4 @@ seed_staging:
|
||||
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%
|
||||
docker compose -f "$COMPOSE_FILE" run --rm seed
|
||||
|
||||
@@ -139,12 +139,11 @@ func (r *HppRepositoryImpl) GetOvkUsageCost(ctx context.Context, projectFlockKan
|
||||
Select("COALESCE(SUM(rs.usage_qty * COALESCE(pi.price, 0)), 0)").
|
||||
Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id").
|
||||
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
|
||||
Joins("JOIN flags AS f ON f.flagable_id = pw.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ?", fifo.UsableKeyRecordingStock.String(), fifo.StockableKeyPurchaseItems.String()).
|
||||
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
|
||||
Where("r.project_flock_kandangs_id IN (?)", projectFlockKandangIDs).
|
||||
Where("r.record_datetime <= ?", *date).
|
||||
Where("f.name IN ?", flags).
|
||||
Where("EXISTS (SELECT 1 FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?)", entity.FlagableTypeProduct, flags).
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
||||
@@ -26,6 +26,7 @@ type FifoService interface {
|
||||
Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error)
|
||||
ReleaseUsage(ctx context.Context, req StockReleaseRequest) error
|
||||
AdjustStockableQuantity(ctx context.Context, req StockAdjustRequest) error
|
||||
ResolvePending(ctx context.Context, req PendingResolveRequest) ([]PendingResolution, error)
|
||||
}
|
||||
|
||||
type fifoService struct {
|
||||
@@ -111,6 +112,11 @@ type PendingResolution struct {
|
||||
Quantity float64
|
||||
}
|
||||
|
||||
type PendingResolveRequest struct {
|
||||
ProductWarehouseID uint
|
||||
Tx *gorm.DB
|
||||
}
|
||||
|
||||
type StockReplenishResult struct {
|
||||
AddedQuantity float64
|
||||
PendingResolved []PendingResolution
|
||||
@@ -147,6 +153,7 @@ type StockReleaseRequest struct {
|
||||
Reason *string
|
||||
Tx *gorm.DB
|
||||
}
|
||||
|
||||
func (s *fifoService) AdjustStockableQuantity(ctx context.Context, req StockAdjustRequest) error {
|
||||
if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" {
|
||||
return errors.New("stockable key and id are required")
|
||||
@@ -226,6 +233,23 @@ func (s *fifoService) Replenish(ctx context.Context, req StockReplenishRequest)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *fifoService) ResolvePending(ctx context.Context, req PendingResolveRequest) ([]PendingResolution, error) {
|
||||
if req.ProductWarehouseID == 0 {
|
||||
return nil, errors.New("product warehouse id is required")
|
||||
}
|
||||
|
||||
var resolved []PendingResolution
|
||||
err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
||||
var err error
|
||||
resolved, err = s.resolvePendingForWarehouse(ctx, tx, req.ProductWarehouseID)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error) {
|
||||
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
|
||||
return nil, errors.New("usable key and id are required")
|
||||
@@ -308,7 +332,7 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
|
||||
}
|
||||
|
||||
if reductionTarget > 0 {
|
||||
released, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, reductionTarget)
|
||||
released, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, reductionTarget, productWarehouseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -355,7 +379,7 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest)
|
||||
}
|
||||
var usageDelta, pendingDelta float64
|
||||
if ctxRow.UsageQty > 0 {
|
||||
if _, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, ctxRow.UsageQty); err != nil {
|
||||
if _, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, ctxRow.UsageQty, ctxRow.ProductWarehouseID); err != nil {
|
||||
return err
|
||||
}
|
||||
usageDelta -= ctxRow.UsageQty
|
||||
@@ -721,6 +745,7 @@ func (s *fifoService) releaseUsagePortion(
|
||||
usableKey fifo.UsableKey,
|
||||
usableID uint,
|
||||
target float64,
|
||||
expectedWarehouseID uint,
|
||||
) (float64, error) {
|
||||
if target <= 0 {
|
||||
return 0, nil
|
||||
@@ -736,6 +761,18 @@ func (s *fifoService) releaseUsagePortion(
|
||||
if len(allocations) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
for i := range allocations {
|
||||
alloc := &allocations[i]
|
||||
if expectedWarehouseID == 0 || alloc.ProductWarehouseId == expectedWarehouseID {
|
||||
continue
|
||||
}
|
||||
if err := tx.Model(&entities.StockAllocation{}).
|
||||
Where("id = ?", alloc.Id).
|
||||
Update("product_warehouse_id", expectedWarehouseID).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
alloc.ProductWarehouseId = expectedWarehouseID
|
||||
}
|
||||
|
||||
var (
|
||||
remaining = target
|
||||
@@ -832,30 +869,30 @@ func (s *fifoService) fetchPendingCandidates(ctx context.Context, tx *gorm.DB, p
|
||||
cfg.Columns.CreatedAt,
|
||||
)
|
||||
|
||||
var rows []struct {
|
||||
ID uint
|
||||
Pending float64
|
||||
CreatedAt time.Time
|
||||
}
|
||||
if cfg.Columns.CreatedAt == cfg.Columns.ID {
|
||||
var rows []struct {
|
||||
ID uint
|
||||
Pending float64 `gorm:"column:pending_qty"`
|
||||
CreatedAt int64 `gorm:"column:created_at"`
|
||||
}
|
||||
|
||||
query := tx.Table(cfg.Table).
|
||||
Select(selectStmt).
|
||||
Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID).
|
||||
Where(fmt.Sprintf("%s > 0", cfg.Columns.PendingQuantity)).
|
||||
Limit(s.pendingBatchPerUsable)
|
||||
query := tx.Table(cfg.Table).
|
||||
Select(selectStmt).
|
||||
Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID).
|
||||
Where(fmt.Sprintf("%s > 0", cfg.Columns.PendingQuantity)).
|
||||
Limit(s.pendingBatchPerUsable)
|
||||
|
||||
if cfg.Scope != nil {
|
||||
query = cfg.Scope(query)
|
||||
}
|
||||
if cfg.Scope != nil {
|
||||
query = cfg.Scope(query)
|
||||
}
|
||||
|
||||
for _, order := range s.orderClauses(cfg.OrderBy) {
|
||||
query = query.Order(order)
|
||||
}
|
||||
for _, order := range s.orderClauses(cfg.OrderBy) {
|
||||
query = query.Order(order)
|
||||
}
|
||||
|
||||
if err := query.Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
if row.Pending <= 0 {
|
||||
continue
|
||||
@@ -865,9 +902,47 @@ func (s *fifoService) fetchPendingCandidates(ctx context.Context, tx *gorm.DB, p
|
||||
Config: cfg,
|
||||
UsableID: row.ID,
|
||||
Pending: row.Pending,
|
||||
CreatedAt: row.CreatedAt,
|
||||
CreatedAt: time.Unix(0, row.CreatedAt),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
var rows []struct {
|
||||
ID uint
|
||||
Pending float64 `gorm:"column:pending_qty"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
}
|
||||
|
||||
query := tx.Table(cfg.Table).
|
||||
Select(selectStmt).
|
||||
Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID).
|
||||
Where(fmt.Sprintf("%s > 0", cfg.Columns.PendingQuantity)).
|
||||
Limit(s.pendingBatchPerUsable)
|
||||
|
||||
if cfg.Scope != nil {
|
||||
query = cfg.Scope(query)
|
||||
}
|
||||
|
||||
for _, order := range s.orderClauses(cfg.OrderBy) {
|
||||
query = query.Order(order)
|
||||
}
|
||||
|
||||
if err := query.Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
if row.Pending <= 0 {
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates, pendingCandidate{
|
||||
UsableKey: key,
|
||||
Config: cfg,
|
||||
UsableID: row.ID,
|
||||
Pending: row.Pending,
|
||||
CreatedAt: row.CreatedAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(candidates) == 0 {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE adjustment_stocks
|
||||
DROP COLUMN adj_number;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,10 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE adjustment_stocks
|
||||
ADD COLUMN adj_number VARCHAR(255);
|
||||
|
||||
UPDATE adjustment_stocks
|
||||
SET adj_number = CONCAT('ADJ-', LPAD(id::text, 5, '0'))
|
||||
WHERE adj_number IS NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,47 @@
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
fallback_fcr_id BIGINT;
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'project_flocks'
|
||||
AND column_name = 'fcr_id'
|
||||
) THEN
|
||||
ALTER TABLE project_flocks
|
||||
ADD COLUMN fcr_id BIGINT;
|
||||
END IF;
|
||||
|
||||
SELECT id INTO fallback_fcr_id
|
||||
FROM fcrs
|
||||
ORDER BY id ASC
|
||||
LIMIT 1;
|
||||
|
||||
IF fallback_fcr_id IS NOT NULL THEN
|
||||
UPDATE project_flocks
|
||||
SET fcr_id = fallback_fcr_id
|
||||
WHERE fcr_id IS NULL;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
ALTER COLUMN fcr_id SET NOT NULL;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'project_flocks_fcr_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE project_flocks
|
||||
DROP CONSTRAINT project_flocks_fcr_id_fkey;
|
||||
END IF;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
ADD CONSTRAINT project_flocks_fcr_id_fkey
|
||||
FOREIGN KEY (fcr_id) REFERENCES fcrs(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,26 @@
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'project_flocks'
|
||||
AND column_name = 'fcr_id'
|
||||
) THEN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'project_flocks_fcr_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE project_flocks
|
||||
DROP CONSTRAINT project_flocks_fcr_id_fkey;
|
||||
END IF;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
DROP COLUMN fcr_id;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
DROP CONSTRAINT IF EXISTS chk_recording_depletions_pending_zero;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
DROP COLUMN IF EXISTS total_used_qty;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
DROP COLUMN IF EXISTS usage_qty;
|
||||
|
||||
COMMIT;
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
ADD COLUMN IF NOT EXISTS total_used_qty numeric(15, 3) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS usage_qty numeric(15, 3) NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE recording_depletions
|
||||
SET pending_qty = 0
|
||||
WHERE pending_qty IS NULL OR pending_qty <> 0;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
ADD CONSTRAINT chk_recording_depletions_pending_zero
|
||||
CHECK (pending_qty = 0);
|
||||
|
||||
COMMIT;
|
||||
@@ -74,7 +74,7 @@ func seedUsers(tx *gorm.DB) (map[string]uint, error) {
|
||||
}
|
||||
|
||||
func seedUoms(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
names := []string{"Kilogram", "Gram", "Liter", "Unit", "Ekor"}
|
||||
names := []string{"Kilogram", "Gram", "Liter", "Unit", "Ekor", "Butir"}
|
||||
result := make(map[string]uint, len(names))
|
||||
|
||||
for _, name := range names {
|
||||
@@ -235,7 +235,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
||||
Name: "Telur Utuh",
|
||||
Brand: "-",
|
||||
Sku: "4",
|
||||
Uom: "Gram",
|
||||
Uom: "Butir",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
Flags: []utils.FlagType{utils.FlagTelurUtuh},
|
||||
@@ -245,7 +245,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
||||
Name: "Telur Pecah",
|
||||
Brand: "-",
|
||||
Sku: "5",
|
||||
Uom: "Gram",
|
||||
Uom: "Butir",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
Flags: []utils.FlagType{utils.FlagTelurPecah},
|
||||
@@ -255,7 +255,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
||||
Name: "Telur Putih",
|
||||
Brand: "-",
|
||||
Sku: "6",
|
||||
Uom: "Gram",
|
||||
Uom: "Butir",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
Flags: []utils.FlagType{utils.FlagTelurPutih},
|
||||
@@ -265,12 +265,32 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
||||
Name: "Telur Retak",
|
||||
Brand: "-",
|
||||
Sku: "7",
|
||||
Uom: "Gram",
|
||||
Uom: "Butir",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
Flags: []utils.FlagType{utils.FlagTelurRetak},
|
||||
IsVisible: false,
|
||||
},
|
||||
{
|
||||
Name: "Telur Papacal",
|
||||
Brand: "-",
|
||||
Sku: "8",
|
||||
Uom: "Butir",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
Flags: []utils.FlagType{utils.FlagTelur},
|
||||
IsVisible: false,
|
||||
},
|
||||
{
|
||||
Name: "Telur Jumbo",
|
||||
Brand: "-",
|
||||
Sku: "9",
|
||||
Uom: "Butir",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
Flags: []utils.FlagType{utils.FlagTelur},
|
||||
IsVisible: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, seed := range seeds {
|
||||
|
||||
@@ -11,6 +11,7 @@ type AdjustmentStock struct {
|
||||
PendingQty float64 `gorm:"column:pending_qty;default:0"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"`
|
||||
AdjNumber string `gorm:"column:adj_number;uniqueIndex;not null"`
|
||||
|
||||
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
StockLog *StockLog `gorm:"polymorphic:Loggable;polymorphicType:LoggableType;polymorphicId:LoggableId;polymorphicValue:ADJUSTMENT"`
|
||||
|
||||
@@ -11,7 +11,6 @@ type ProjectFlock struct {
|
||||
FlockName string `gorm:"type:varchar(255);not null;uniqueIndex"`
|
||||
AreaId uint `gorm:"not null"`
|
||||
Category string `gorm:"type:varchar(20);not null"`
|
||||
FcrId uint `gorm:"not null"`
|
||||
ProductionStandardId uint `gorm:"column:production_standard_id"`
|
||||
LocationId uint `gorm:"not null"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
@@ -20,7 +19,6 @@ type ProjectFlock struct {
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Area Area `gorm:"foreignKey:AreaId;references:Id"`
|
||||
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
|
||||
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
|
||||
@@ -21,6 +21,7 @@ type PurchaseItem struct {
|
||||
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
ExpenseNonstockId *uint64
|
||||
HasChickin bool `gorm:"-" json:"-"`
|
||||
|
||||
// Relations
|
||||
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
||||
|
||||
@@ -12,7 +12,9 @@ type Recording struct {
|
||||
RecordDatetime time.Time `gorm:"column:record_datetime;not null"`
|
||||
Day *int `gorm:"column:day"`
|
||||
TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"`
|
||||
TotalDepletionCumQty *float64 `gorm:"-"`
|
||||
CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"`
|
||||
DepletionRate *float64 `gorm:"-"`
|
||||
CumIntake *int `gorm:"column:cum_intake"`
|
||||
FcrValue *float64 `gorm:"column:fcr_value"`
|
||||
TotalChickQty *float64 `gorm:"column:total_chick_qty"`
|
||||
|
||||
@@ -6,6 +6,7 @@ type RecordingDepletion struct {
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
SourceProductWarehouseId *uint `gorm:"column:source_product_warehouse_id"`
|
||||
Qty float64 `gorm:"column:qty;not null"`
|
||||
UsageQty float64 `gorm:"column:usage_qty"`
|
||||
PendingQty float64 `gorm:"column:pending_qty"`
|
||||
|
||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
|
||||
@@ -347,12 +347,12 @@ func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||
}
|
||||
|
||||
result, err := u.SapronakService.GetSapronakByProject(c, uint(projectID), flag)
|
||||
result, productFlags, err := u.SapronakService.GetSapronakByProject(c, uint(projectID), flag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := dto.ToSapronakProjectAggregatedFromReports(result, flag)
|
||||
payload := dto.ToSapronakProjectAggregatedFromReports(result, flag, productFlags)
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
@@ -377,12 +377,12 @@ func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||
}
|
||||
|
||||
result, err := u.SapronakService.GetSapronakByKandang(c, uint(projectID), uint(pfkID), flag)
|
||||
result, productFlags, err := u.SapronakService.GetSapronakByKandang(c, uint(projectID), uint(pfkID), flag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := dto.ToSapronakProjectAggregatedFromReport(result, flag)
|
||||
payload := dto.ToSapronakProjectAggregatedFromReport(result, flag, productFlags)
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -127,7 +128,7 @@ type UomSummaryDTO struct {
|
||||
|
||||
// === Mapper Functions for Aggregated Sapronak Response ===
|
||||
|
||||
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||
func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string, productFlags map[uint][]string) SapronakProjectAggregatedDTO {
|
||||
result := SapronakProjectAggregatedDTO{}
|
||||
|
||||
if len(reports) == 0 {
|
||||
@@ -135,10 +136,10 @@ func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag st
|
||||
}
|
||||
|
||||
rep := reports[0]
|
||||
return ToSapronakProjectAggregatedFromReport(&rep, flag)
|
||||
return ToSapronakProjectAggregatedFromReport(&rep, flag, productFlags)
|
||||
}
|
||||
|
||||
func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag string) SapronakProjectAggregatedDTO {
|
||||
func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag string, productFlags map[uint][]string) SapronakProjectAggregatedDTO {
|
||||
result := SapronakProjectAggregatedDTO{}
|
||||
|
||||
if report == nil {
|
||||
@@ -175,6 +176,53 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
||||
return t.Format("02-Jan-2006")
|
||||
}
|
||||
|
||||
flagOrder := map[string]int{
|
||||
"DOC": 0,
|
||||
"PAKAN": 0,
|
||||
"OVK": 0,
|
||||
"PULLET": 0,
|
||||
}
|
||||
|
||||
buildFlagList := func(productID uint, fallback string) string {
|
||||
rawFlags := productFlags[productID]
|
||||
if len(rawFlags) == 0 {
|
||||
if fallback == "" {
|
||||
return ""
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
seen := make(map[string]struct{}, len(rawFlags))
|
||||
ordered := make([]string, 0, len(rawFlags))
|
||||
for _, f := range rawFlags {
|
||||
flagName := strings.ToUpper(strings.TrimSpace(f))
|
||||
if flagName == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[flagName]; ok {
|
||||
continue
|
||||
}
|
||||
seen[flagName] = struct{}{}
|
||||
ordered = append(ordered, flagName)
|
||||
}
|
||||
sort.SliceStable(ordered, func(i, j int) bool {
|
||||
li := ordered[i]
|
||||
lj := ordered[j]
|
||||
ri, iok := flagOrder[li]
|
||||
rj, jok := flagOrder[lj]
|
||||
if iok != jok {
|
||||
if iok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if iok && jok && ri != rj {
|
||||
return ri < rj
|
||||
}
|
||||
return li < lj
|
||||
})
|
||||
return strings.Join(ordered, " ")
|
||||
}
|
||||
|
||||
for _, group := range report.Groups {
|
||||
flagKey := normalizeFlag(group.Flag)
|
||||
ptr := byFlag[flagKey]
|
||||
@@ -206,7 +254,7 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
||||
Date: formatDate(item.Tanggal),
|
||||
ReferenceNumber: item.NoReferensi,
|
||||
Description: item.ProductName,
|
||||
ProductCategory: item.ProductName,
|
||||
ProductCategory: buildFlagList(item.ProductID, flagKey),
|
||||
UnitPrice: item.Harga,
|
||||
Notes: "-",
|
||||
}
|
||||
@@ -234,14 +282,14 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
|
||||
row.Notes = "TRANSFER STOCK"
|
||||
}
|
||||
}
|
||||
case "pemakaian", "adjustment keluar":
|
||||
case "pemakaian":
|
||||
price := row.UnitPrice
|
||||
if price == 0 {
|
||||
price = item.Harga
|
||||
}
|
||||
row.QtyUsed += item.QtyKeluar
|
||||
row.TotalAmount += item.QtyKeluar * price
|
||||
case "mutasi keluar", "penjualan":
|
||||
case "adjustment keluar", "mutasi keluar", "penjualan":
|
||||
price := row.UnitPrice
|
||||
if price == 0 {
|
||||
price = item.Harga
|
||||
|
||||
@@ -24,7 +24,6 @@ type ClosingRepository interface {
|
||||
SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error)
|
||||
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
|
||||
SumRecordingEggQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, error)
|
||||
GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error)
|
||||
GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error)
|
||||
FetchSapronakIncoming(ctx context.Context, kandangID uint) ([]SapronakIncomingRow, error)
|
||||
FetchSapronakIncomingDetails(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, error)
|
||||
@@ -36,6 +35,7 @@ type ClosingRepository interface {
|
||||
FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakSales(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error)
|
||||
FetchSapronakSalesAllocatedDetails(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error)
|
||||
GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error)
|
||||
}
|
||||
|
||||
@@ -101,12 +101,12 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak
|
||||
if len(params.WarehouseIDs) == 0 {
|
||||
return []SapronakRow{}, 0, nil
|
||||
}
|
||||
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL)
|
||||
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
|
||||
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL)
|
||||
args = append(args, params.WarehouseIDs, params.WarehouseIDs, params.WarehouseIDs)
|
||||
case validation.SapronakTypeOutgoing:
|
||||
if len(params.WarehouseIDs) > 0 {
|
||||
unionParts = append(unionParts, sapronakOutgoingTransfersSQL)
|
||||
args = append(args, params.WarehouseIDs)
|
||||
unionParts = append(unionParts, sapronakOutgoingTransfersSQL, sapronakOutgoingAdjustmentsSQL)
|
||||
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
|
||||
}
|
||||
if len(params.ProjectFlockKandangIDs) > 0 {
|
||||
unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
|
||||
@@ -173,12 +173,12 @@ func (r *ClosingRepositoryImpl) GetSapronakSummary(ctx context.Context, params S
|
||||
if len(params.WarehouseIDs) == 0 {
|
||||
return []SapronakSummaryRow{}, nil
|
||||
}
|
||||
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL)
|
||||
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
|
||||
unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL)
|
||||
args = append(args, params.WarehouseIDs, params.WarehouseIDs, params.WarehouseIDs)
|
||||
case validation.SapronakTypeOutgoing:
|
||||
if len(params.WarehouseIDs) > 0 {
|
||||
unionParts = append(unionParts, sapronakOutgoingTransfersSQL)
|
||||
args = append(args, params.WarehouseIDs)
|
||||
unionParts = append(unionParts, sapronakOutgoingTransfersSQL, sapronakOutgoingAdjustmentsSQL)
|
||||
args = append(args, params.WarehouseIDs, params.WarehouseIDs)
|
||||
}
|
||||
if len(params.ProjectFlockKandangIDs) > 0 {
|
||||
unionParts = append(unionParts, sapronakOutgoingMarketingsSQL)
|
||||
@@ -392,22 +392,6 @@ func (r *ClosingRepositoryImpl) SumRecordingEggQtyByProjectFlockKandangIDsAndFla
|
||||
return agg.TotalQty, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetFcrStandardsByFcrID(ctx context.Context, fcrID uint) ([]entity.FcrStandard, error) {
|
||||
if fcrID == 0 {
|
||||
return []entity.FcrStandard{}, nil
|
||||
}
|
||||
|
||||
var standards []entity.FcrStandard
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Where("fcr_id = ?", fcrID).
|
||||
Order("weight ASC").
|
||||
Find(&standards).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return standards, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetExpeditionHPP(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]ExpeditionHPPRow, error) {
|
||||
db := r.DB().WithContext(ctx)
|
||||
|
||||
@@ -455,7 +439,7 @@ SELECT
|
||||
COALESCE(pi.received_date, '1970-01-01') AS sort_date,
|
||||
COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text,
|
||||
COALESCE(p.po_number, '') AS reference_number,
|
||||
'Purchase' AS transaction_type,
|
||||
'Pembelian' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
COALESCE((
|
||||
SELECT string_agg(
|
||||
@@ -504,7 +488,7 @@ SELECT
|
||||
st.transfer_date AS sort_date,
|
||||
TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
|
||||
st.movement_number AS reference_number,
|
||||
'Internal Transfer In' AS transaction_type,
|
||||
'Mutasi' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
COALESCE((
|
||||
SELECT string_agg(
|
||||
@@ -538,7 +522,7 @@ SELECT
|
||||
std.usage_qty AS quantity,
|
||||
u.id AS unit_id,
|
||||
u.name AS unit,
|
||||
'Stock Refill' AS notes
|
||||
st.reason AS notes
|
||||
FROM stock_transfer_details std
|
||||
JOIN stock_transfers st ON st.id = std.stock_transfer_id
|
||||
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
|
||||
@@ -548,13 +532,63 @@ JOIN uoms u ON u.id = prod.uom_id
|
||||
WHERE st.to_warehouse_id IN ?
|
||||
`
|
||||
|
||||
sapronakIncomingAdjustmentsSQL = `
|
||||
SELECT
|
||||
CAST(ast.id AS BIGINT) AS id,
|
||||
ast.created_at AS sort_date,
|
||||
COALESCE(TO_CHAR(ast.created_at, 'DD-Mon-YYYY'), '') AS date_text,
|
||||
COALESCE(ast.adj_number, '') AS reference_number,
|
||||
'Adjustment stock' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
COALESCE((
|
||||
SELECT string_agg(
|
||||
f.name,
|
||||
' ' ORDER BY
|
||||
CASE
|
||||
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
f.name
|
||||
)
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(
|
||||
f.name,
|
||||
' ' ORDER BY
|
||||
CASE
|
||||
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
f.name
|
||||
)
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
'-' AS source_warehouse,
|
||||
COALESCE(w.name, '') AS destination_warehouse,
|
||||
'' AS destination,
|
||||
COALESCE(ast.total_qty, 0) AS quantity,
|
||||
u.id AS unit_id,
|
||||
u.name AS unit,
|
||||
'-' AS notes
|
||||
FROM adjustment_stocks ast
|
||||
JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id
|
||||
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||
JOIN products prod ON prod.id = pw.product_id
|
||||
JOIN uoms u ON u.id = prod.uom_id
|
||||
WHERE pw.warehouse_id IN ?
|
||||
AND COALESCE(ast.total_qty, 0) <> 0
|
||||
`
|
||||
|
||||
sapronakOutgoingTransfersSQL = `
|
||||
SELECT
|
||||
CAST(st.id AS BIGINT) AS id,
|
||||
st.transfer_date AS sort_date,
|
||||
TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text,
|
||||
st.movement_number AS reference_number,
|
||||
'Internal Transfer Out' AS transaction_type,
|
||||
'Mutasi' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
COALESCE((
|
||||
SELECT string_agg(
|
||||
@@ -588,7 +622,7 @@ SELECT
|
||||
std.usage_qty AS quantity,
|
||||
u.id AS unit_id,
|
||||
u.name AS unit,
|
||||
'Transfer to other unit' AS notes
|
||||
st.reason AS notes
|
||||
FROM stock_transfer_details std
|
||||
JOIN stock_transfers st ON st.id = std.stock_transfer_id
|
||||
LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id
|
||||
@@ -598,13 +632,70 @@ JOIN uoms u ON u.id = prod.uom_id
|
||||
WHERE st.from_warehouse_id IN ?
|
||||
`
|
||||
|
||||
sapronakOutgoingAdjustmentsSQL = `
|
||||
SELECT
|
||||
CAST(ast.id AS BIGINT) AS id,
|
||||
ast.created_at AS sort_date,
|
||||
COALESCE(TO_CHAR(ast.created_at, 'DD-Mon-YYYY'), '') AS date_text,
|
||||
COALESCE(ast.adj_number, '') AS reference_number,
|
||||
'Adjustment stock' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
COALESCE((
|
||||
SELECT string_agg(
|
||||
f.name,
|
||||
' ' ORDER BY
|
||||
CASE
|
||||
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
f.name
|
||||
)
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_category,
|
||||
COALESCE((
|
||||
SELECT string_agg(
|
||||
f.name,
|
||||
' ' ORDER BY
|
||||
CASE
|
||||
WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
f.name
|
||||
)
|
||||
FROM flags f
|
||||
WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id
|
||||
), '') AS product_sub_category,
|
||||
COALESCE(w.name, '') AS source_warehouse,
|
||||
'-' AS destination_warehouse,
|
||||
'' AS destination,
|
||||
COALESCE(ast.usage_qty, 0) AS quantity,
|
||||
u.id AS unit_id,
|
||||
u.name AS unit,
|
||||
'-' AS notes
|
||||
FROM adjustment_stocks ast
|
||||
JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id
|
||||
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||
JOIN products prod ON prod.id = pw.product_id
|
||||
JOIN uoms u ON u.id = prod.uom_id
|
||||
WHERE pw.warehouse_id IN ?
|
||||
AND COALESCE(ast.usage_qty, 0) <> 0
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM flags f
|
||||
WHERE f.flagable_id = pw.product_id
|
||||
AND f.flagable_type = 'products'
|
||||
AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK')
|
||||
)
|
||||
`
|
||||
|
||||
sapronakOutgoingMarketingsSQL = `
|
||||
SELECT
|
||||
CAST(mp.id AS BIGINT) AS id,
|
||||
m.so_date AS sort_date,
|
||||
TO_CHAR(m.so_date, 'DD-Mon-YYYY') AS date_text,
|
||||
m.so_number AS reference_number,
|
||||
'Trading Sales' AS transaction_type,
|
||||
'Penjualan' AS transaction_type,
|
||||
prod.name AS product_name,
|
||||
COALESCE((
|
||||
SELECT string_agg(
|
||||
@@ -652,7 +743,7 @@ WHERE pw.project_flock_kandang_id IN ?
|
||||
FROM flags f
|
||||
WHERE f.flagable_id = pw.product_id
|
||||
AND f.flagable_type = 'products'
|
||||
AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET')
|
||||
AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK')
|
||||
)
|
||||
`
|
||||
)
|
||||
@@ -900,8 +991,8 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.C
|
||||
query := r.withCtx(ctx).
|
||||
Table("stock_allocations AS sa").
|
||||
Select(`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
p_resolve.id AS product_id,
|
||||
p_resolve.name AS product_name,
|
||||
f.name AS flag,
|
||||
COALESCE(
|
||||
pi.received_date,
|
||||
@@ -922,10 +1013,9 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.C
|
||||
) AS reference,
|
||||
0 AS qty_in,
|
||||
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
||||
COALESCE(pi.price, p.product_price, 0) AS price
|
||||
COALESCE(pi.price, p_resolve.product_price, 0) AS price
|
||||
`).
|
||||
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("LEFT JOIN recording_stocks rs ON rs.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyRecordingStock.String()).
|
||||
Joins("LEFT JOIN recordings r ON r.id = rs.recording_id").
|
||||
Joins("LEFT JOIN project_chickins pc_used ON pc_used.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyProjectChickin.String()).
|
||||
@@ -935,10 +1025,13 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.C
|
||||
Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id").
|
||||
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
|
||||
Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
|
||||
Joins("LEFT JOIN product_warehouses pw_ltt ON pw_ltt.id = ltt.product_warehouse_id").
|
||||
Joins("LEFT JOIN adjustment_stocks ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()).
|
||||
Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
||||
Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
||||
Joins("LEFT JOIN products p_resolve ON p_resolve.id = COALESCE(pi.product_id, pw_ltt.product_id, pw.product_id)").
|
||||
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
||||
Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
||||
Where("f.name IN ?", sapronakFlagsAll).
|
||||
Where(`
|
||||
(sa.usable_type = ? AND r.project_flock_kandangs_id = ?)
|
||||
@@ -948,12 +1041,12 @@ func (r *ClosingRepositoryImpl) FetchSapronakUsageAllocatedDetails(ctx context.C
|
||||
fifo.UsableKeyRecordingStock.String(), projectFlockKandangID,
|
||||
fifo.UsableKeyProjectChickin.String(), projectFlockKandangID,
|
||||
)
|
||||
query = r.joinSapronakProductFlag(query, "p").
|
||||
query = r.joinSapronakProductFlag(query, "p_resolve").
|
||||
Group(`
|
||||
pw.product_id, p.name, f.name,
|
||||
p_resolve.id, p_resolve.name, f.name,
|
||||
pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at, pc.chick_in_date, r.record_datetime,
|
||||
po.po_number, st.movement_number, lt.transfer_number, ast.id, pc.id, r.id,
|
||||
pi.price, p.product_price
|
||||
pi.price, p_resolve.product_price
|
||||
`)
|
||||
|
||||
return scanAndGroupDetails(query)
|
||||
@@ -1085,12 +1178,75 @@ func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||
rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeAdjustment), false)
|
||||
poByWarehouse := r.DB().
|
||||
Table("purchase_items pi").
|
||||
Select("DISTINCT ON (pi.product_warehouse_id) pi.product_warehouse_id, po.po_number, pi.received_date").
|
||||
Joins("JOIN purchases po ON po.id = pi.purchase_id").
|
||||
Where("pi.received_date IS NOT NULL").
|
||||
Order("pi.product_warehouse_id, pi.received_date ASC")
|
||||
|
||||
incomingQuery := r.withCtx(ctx).
|
||||
Table("adjustment_stocks AS ast").
|
||||
Select(`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
ast.created_at AS date,
|
||||
CONCAT('ADJ-', ast.id) AS reference,
|
||||
COALESCE(ast.total_qty, 0) AS qty_in,
|
||||
0 AS qty_out,
|
||||
COALESCE(p.product_price, 0) AS price
|
||||
`).
|
||||
Joins("JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id").
|
||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("f.name IN ?", sapronakFlagsAll).
|
||||
Where("COALESCE(ast.total_qty, 0) > 0")
|
||||
incomingQuery = r.joinSapronakProductFlag(incomingQuery, "p")
|
||||
incoming, err := scanAndGroupDetails(incomingQuery)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
in, out := splitStockLogs(rows, func(row stockLogSapronakRow) string { return fmt.Sprintf("ADJ-%d", row.ID) })
|
||||
return in, out, nil
|
||||
|
||||
outgoingQuery := r.withCtx(ctx).
|
||||
Table("stock_allocations AS sa").
|
||||
Select(`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
COALESCE(pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at) AS date,
|
||||
COALESCE(po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, CONCAT('CHICKIN-', pc.id), CONCAT('ADJ-', ast_in.id), CONCAT('ADJ-', ast.id)) AS reference,
|
||||
0 AS qty_in,
|
||||
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
||||
COALESCE(p.product_price, 0) AS price
|
||||
`).
|
||||
Joins("JOIN adjustment_stocks ast ON ast.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyAdjustmentOut.String()).
|
||||
Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
|
||||
Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id").
|
||||
Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
|
||||
Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id").
|
||||
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
|
||||
Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
|
||||
Joins("LEFT JOIN adjustment_stocks ast_in ON ast_in.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()).
|
||||
Joins("LEFT JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
||||
Joins("LEFT JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
||||
Joins("LEFT JOIN (?) pfp_po ON pfp_po.product_warehouse_id = pfp.product_warehouse_id", poByWarehouse).
|
||||
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
||||
Joins("JOIN warehouses w ON w.id = pw.warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
||||
Where("w.kandang_id = ?", kandangID).
|
||||
Where("f.name IN ?", sapronakFlagsAll).
|
||||
Where("f.name NOT IN ?", sapronakFlags(utils.FlagDOC, utils.FlagPullet)).
|
||||
Group("pw.product_id, p.name, f.name, pi.received_date, st.transfer_date, lt.transfer_date, pfp_po.received_date, pc.chick_in_date, ast_in.created_at, ast.created_at, po.po_number, st.movement_number, lt.transfer_number, pfp_po.po_number, pc.id, ast_in.id, ast.id, p.product_price")
|
||||
outgoingQuery = r.joinSapronakProductFlag(outgoingQuery, "p")
|
||||
outgoing, err := scanAndGroupDetails(outgoingQuery)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return incoming, outgoing, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||
@@ -1286,6 +1442,59 @@ func (r *ClosingRepositoryImpl) FetchSapronakSales(ctx context.Context, projectF
|
||||
return sales, nil
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakSalesAllocatedDetails(ctx context.Context, projectFlockKandangID uint) (map[uint][]SapronakDetailRow, error) {
|
||||
if projectFlockKandangID == 0 {
|
||||
return map[uint][]SapronakDetailRow{}, nil
|
||||
}
|
||||
|
||||
query := r.withCtx(ctx).
|
||||
Table("stock_allocations AS sa").
|
||||
Select(`
|
||||
pw.product_id AS product_id,
|
||||
p.name AS product_name,
|
||||
f.name AS flag,
|
||||
COALESCE(
|
||||
pi.received_date,
|
||||
st.transfer_date,
|
||||
lt.transfer_date,
|
||||
ast.created_at
|
||||
) AS date,
|
||||
COALESCE(
|
||||
po.po_number,
|
||||
st.movement_number,
|
||||
lt.transfer_number,
|
||||
CONCAT('ADJ-', ast.id),
|
||||
''
|
||||
) AS reference,
|
||||
0 AS qty_in,
|
||||
COALESCE(SUM(sa.qty), 0) AS qty_out,
|
||||
COALESCE(pi.price, p.product_price, 0) AS price
|
||||
`).
|
||||
Joins("JOIN product_warehouses pw ON pw.id = sa.product_warehouse_id").
|
||||
Joins("JOIN products p ON p.id = pw.product_id").
|
||||
Joins("JOIN marketing_delivery_products mdp ON mdp.id = sa.usable_id AND sa.usable_type = ?", fifo.UsableKeyMarketingDelivery.String()).
|
||||
Joins("LEFT JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
|
||||
Joins("LEFT JOIN purchases po ON po.id = pi.purchase_id").
|
||||
Joins("LEFT JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
|
||||
Joins("LEFT JOIN stock_transfers st ON st.id = std.stock_transfer_id").
|
||||
Joins("LEFT JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
|
||||
Joins("LEFT JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id").
|
||||
Joins("LEFT JOIN adjustment_stocks ast ON ast.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyAdjustmentIn.String()).
|
||||
Where("sa.status = ?", entity.StockAllocationStatusActive).
|
||||
Where("sa.stockable_type <> ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
||||
Where("pw.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||
Where("f.name IN ?", sapronakFlagsAll).
|
||||
Group(`
|
||||
pw.product_id, p.name, f.name,
|
||||
pi.received_date, st.transfer_date, lt.transfer_date, ast.created_at,
|
||||
po.po_number, st.movement_number, lt.transfer_number, ast.id,
|
||||
pi.price, p.product_price
|
||||
`)
|
||||
|
||||
query = r.joinSapronakProductFlag(query, "p")
|
||||
return scanAndGroupDetails(query)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) GetProductsWithFlagsByIDs(ctx context.Context, productIDs []uint) ([]entity.Product, error) {
|
||||
if len(productIDs) == 0 {
|
||||
return []entity.Product{}, nil
|
||||
|
||||
@@ -836,14 +836,6 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
||||
|
||||
finalPopulation := population - claimCulling
|
||||
|
||||
var standards []entity.FcrStandard
|
||||
if project.FcrId > 0 {
|
||||
standards, err = s.Repository.GetFcrStandardsByFcrID(c.Context(), project.FcrId)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch FCR standards for project flock %d: %+v", projectFlockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch FCR standard data")
|
||||
}
|
||||
}
|
||||
age, err := s.calculateAverageSalesAge(c.Context(), projectFlockID, kandangID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to calculate sales age for project flock %d: %+v", projectFlockID, err)
|
||||
@@ -893,7 +885,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
||||
chickenDepletion = 0
|
||||
}
|
||||
|
||||
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards)
|
||||
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age)
|
||||
if fcrActFromRecording != nil {
|
||||
chickenPerformance.FcrAct = *fcrActFromRecording
|
||||
}
|
||||
@@ -943,7 +935,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
||||
eggDepletion = 0
|
||||
}
|
||||
|
||||
eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards)
|
||||
eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age)
|
||||
if fcrActFromRecording != nil {
|
||||
eggPerf.FcrAct = *fcrActFromRecording
|
||||
}
|
||||
@@ -1001,10 +993,10 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
|
||||
performance.EggMass = eggMass
|
||||
}
|
||||
}
|
||||
performance.DeffFcr = performance.FcrStd - performance.FcrAct
|
||||
if productionStandardDetail != nil {
|
||||
if productionStandardDetail.StandardFCR != nil {
|
||||
performance.FcrStd = *productionStandardDetail.StandardFCR
|
||||
performance.DeffFcr = performance.FcrStd - performance.FcrAct
|
||||
}
|
||||
if !isGrowing {
|
||||
if productionStandardDetail.TargetHenDayProduction != nil {
|
||||
@@ -1091,8 +1083,8 @@ func (s closingService) determineProductionWeek(ctx context.Context, projectFloc
|
||||
return week, nil
|
||||
}
|
||||
|
||||
func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO {
|
||||
mortalityStd, fcrStd := closestFcrValues(standards, averageWeight)
|
||||
func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64) dto.ClosingPerformanceDTO {
|
||||
mortalityStd, fcrStd := 0.0, 0.0
|
||||
|
||||
fcrAct := 0.0
|
||||
if totalWeight > 0 {
|
||||
@@ -1124,21 +1116,3 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul
|
||||
AwgAct: awg,
|
||||
}
|
||||
}
|
||||
|
||||
func closestFcrValues(standards []entity.FcrStandard, averageWeight float64) (float64, float64) {
|
||||
if len(standards) == 0 || averageWeight <= 0 {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
closest := standards[0]
|
||||
minDiff := math.Abs(closest.Weight - averageWeight)
|
||||
for _, std := range standards[1:] {
|
||||
diff := math.Abs(std.Weight - averageWeight)
|
||||
if diff < minDiff {
|
||||
minDiff = diff
|
||||
closest = std
|
||||
}
|
||||
}
|
||||
|
||||
return closest.Mortality, closest.FcrNumber
|
||||
}
|
||||
|
||||
@@ -294,6 +294,9 @@ func (s closingKeuanganService) calculateProductionData(c *fiber.Ctx, projectFlo
|
||||
func (s closingKeuanganService) buildHPPSection(c *fiber.Ctx, projectFlock *entity.ProjectFlock, projectFlockKandangs []entity.ProjectFlockKandang, costs *CostData, production *ProductionData) dto.HPPSection {
|
||||
|
||||
actualPopulation := production.TotalPopulationIn - production.TotalDepletion
|
||||
if lastPopulation, ok := s.getLastPopulationFromRecordings(c, projectFlockKandangs); ok {
|
||||
actualPopulation = lastPopulation
|
||||
}
|
||||
totalWeightProduced := production.TotalWeightProduced
|
||||
totalEggWeightKg := production.TotalEggWeightKg
|
||||
|
||||
@@ -529,6 +532,35 @@ func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.Proj
|
||||
return dto.ToProfitLossSection(plItems, plSummary)
|
||||
}
|
||||
|
||||
func (s closingKeuanganService) getLastPopulationFromRecordings(c *fiber.Ctx, projectFlockKandangs []entity.ProjectFlockKandang) (float64, bool) {
|
||||
if s.RecordingRepo == nil || len(projectFlockKandangs) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
total := 0.0
|
||||
recordedCount := 0
|
||||
for _, kandang := range projectFlockKandangs {
|
||||
latest, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(c.Context(), kandang.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch latest recording for project_flock_kandang_id=%d: %+v", kandang.Id, err)
|
||||
return 0, false
|
||||
}
|
||||
if latest == nil || latest.TotalChickQty == nil {
|
||||
continue
|
||||
}
|
||||
recordedCount++
|
||||
if *latest.TotalChickQty > 0 {
|
||||
total += *latest.TotalChickQty
|
||||
}
|
||||
}
|
||||
|
||||
if recordedCount != len(projectFlockKandangs) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return total, true
|
||||
}
|
||||
|
||||
func containsFlag(flags []entity.Flag, name string) bool {
|
||||
for _, flag := range flags {
|
||||
if flag.Name == name {
|
||||
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
)
|
||||
|
||||
type SapronakService interface {
|
||||
GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error)
|
||||
GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error)
|
||||
GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, map[uint][]string, error)
|
||||
GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, map[uint][]string, error)
|
||||
}
|
||||
|
||||
type sapronakService struct {
|
||||
@@ -42,9 +42,9 @@ func NewSapronakService(
|
||||
}
|
||||
}
|
||||
|
||||
func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error) {
|
||||
func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, map[uint][]string, error) {
|
||||
if projectFlockID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id is required")
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id is required")
|
||||
}
|
||||
reports, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
|
||||
ProjectFlockID: projectFlockID,
|
||||
@@ -52,19 +52,27 @@ func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint,
|
||||
Flag: flag,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(reports) <= 1 {
|
||||
return reports, nil
|
||||
flags, err := s.collectProductFlags(c.Context(), reports)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return reports, flags, nil
|
||||
}
|
||||
|
||||
combined := s.combineSapronakReports(reports, projectFlockID)
|
||||
return []dto.SapronakReportDTO{combined}, nil
|
||||
flags, err := s.collectProductFlags(c.Context(), []dto.SapronakReportDTO{combined})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return []dto.SapronakReportDTO{combined}, flags, nil
|
||||
}
|
||||
|
||||
func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error) {
|
||||
func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, map[uint][]string, error) {
|
||||
if projectFlockID == 0 || pfkID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id and project_flock_kandang_id are required")
|
||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id and project_flock_kandang_id are required")
|
||||
}
|
||||
|
||||
results, err := s.computeSapronakReports(c.Context(), &validation.CountSapronakQuery{
|
||||
@@ -74,16 +82,20 @@ func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint,
|
||||
Flag: flag,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, res := range results {
|
||||
if res.ProjectFlockID == projectFlockID && res.ProjectFlockKandangID == pfkID {
|
||||
return &res, nil
|
||||
flags, err := s.collectProductFlags(c.Context(), []dto.SapronakReportDTO{res})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &res, flags, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Sapronak for kandang not found")
|
||||
return nil, nil, fiber.NewError(fiber.StatusNotFound, "Sapronak for kandang not found")
|
||||
}
|
||||
|
||||
func (s sapronakService) computeSapronakReports(ctx context.Context, params *validation.CountSapronakQuery) ([]dto.SapronakReportDTO, error) {
|
||||
@@ -136,6 +148,52 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (s sapronakService) collectProductFlags(ctx context.Context, reports []dto.SapronakReportDTO) (map[uint][]string, error) {
|
||||
productIDs := make(map[uint]struct{})
|
||||
for _, report := range reports {
|
||||
for _, group := range report.Groups {
|
||||
for _, item := range group.Items {
|
||||
if item.ProductID > 0 {
|
||||
productIDs[item.ProductID] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(productIDs) == 0 {
|
||||
return map[uint][]string{}, nil
|
||||
}
|
||||
|
||||
ids := make([]uint, 0, len(productIDs))
|
||||
for id := range productIDs {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
products, err := s.Repository.GetProductsWithFlagsByIDs(ctx, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[uint][]string, len(products))
|
||||
for _, product := range products {
|
||||
if len(product.Flags) == 0 {
|
||||
continue
|
||||
}
|
||||
flags := make([]string, 0, len(product.Flags))
|
||||
for _, flag := range product.Flags {
|
||||
name := strings.TrimSpace(flag.Name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
flags = append(flags, strings.ToUpper(name))
|
||||
}
|
||||
if len(flags) > 0 {
|
||||
result[product.Id] = flags
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s sapronakService) loadProjectFlockKandangs(ctx context.Context, params *validation.CountSapronakQuery) ([]entity.ProjectFlockKandang, error) {
|
||||
db := s.ProjectFlockKandangRepo.DB().WithContext(ctx).
|
||||
Preload("ProjectFlock").
|
||||
@@ -363,7 +421,7 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
salesOutRows, err := s.Repository.FetchSapronakSales(ctx, pfk.Id)
|
||||
salesOutRows, err := s.Repository.FetchSapronakSalesAllocatedDetails(ctx, pfk.Id)
|
||||
if err != nil {
|
||||
return nil, nil, 0, 0, err
|
||||
}
|
||||
@@ -570,13 +628,12 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
|
||||
if existing.ProductName == "" {
|
||||
existing.ProductName = d.ProductName
|
||||
}
|
||||
existing.UsageQty += d.QtyKeluar
|
||||
existing.UsageValue += d.Nilai
|
||||
if existing.IncomingQty >= existing.UsageQty {
|
||||
existing.RemainingQty = existing.IncomingQty - existing.UsageQty
|
||||
} else {
|
||||
existing.RemainingQty = 0
|
||||
// Adjustment keluar should reduce stock without inflating usage-based HPP.
|
||||
remaining := existing.IncomingQty - existing.UsageQty - d.QtyKeluar
|
||||
if remaining < 0 {
|
||||
remaining = 0
|
||||
}
|
||||
existing.RemainingQty = remaining
|
||||
itemMap[productID] = existing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,16 +107,23 @@ func applyDashboardFilters(db *gorm.DB, filters *validation.DashboardFilter) *go
|
||||
func (r *DashboardRepositoryImpl) GetRecordingWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]RecordingWeeklyMetric, error) {
|
||||
var rows []RecordingWeeklyMetric
|
||||
|
||||
weekExpr := `CASE
|
||||
WHEN r.day IS NULL OR r.day <= 0 THEN 1
|
||||
WHEN UPPER(pf.category) = 'LAYING' THEN ((r.day - 1) / 7 + 1) + 17
|
||||
ELSE ((r.day - 1) / 7 + 1)
|
||||
END`
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("recordings AS r").
|
||||
Select(`((r.day - 1) / 7 + 1) AS week,
|
||||
Select(fmt.Sprintf(`%s AS week,
|
||||
COALESCE(AVG(r.hen_day), 0) AS hen_day,
|
||||
COALESCE(AVG(r.egg_weight), 0) AS egg_weight,
|
||||
COALESCE(AVG(r.feed_intake), 0) AS feed_intake,
|
||||
COALESCE(AVG(r.fcr_value), 0) AS fcr_value,
|
||||
COALESCE(AVG(r.cum_depletion_rate), 0) AS cum_depletion_rate`).
|
||||
COALESCE(AVG(r.cum_depletion_rate), 0) AS cum_depletion_rate`, weekExpr)).
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
|
||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||
Where("r.deleted_at IS NULL").
|
||||
Where("r.day IS NOT NULL AND r.day > 0")
|
||||
@@ -188,92 +195,19 @@ func (r *DashboardRepositoryImpl) GetStandardFcrWeekly(ctx context.Context, week
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
filterClause := ""
|
||||
filterArgs := make([]interface{}, 0)
|
||||
if filters != nil {
|
||||
if len(filters.FlockIds) > 0 {
|
||||
filterClause += " AND pf.id IN ?"
|
||||
filterArgs = append(filterArgs, filters.FlockIds)
|
||||
}
|
||||
if len(filters.KandangIds) > 0 {
|
||||
filterClause += " AND k.id IN ?"
|
||||
filterArgs = append(filterArgs, filters.KandangIds)
|
||||
}
|
||||
if len(filters.LokasiIds) > 0 {
|
||||
filterClause += " AND k.location_id IN ?"
|
||||
filterArgs = append(filterArgs, filters.LokasiIds)
|
||||
}
|
||||
standardIDs := r.standardIDSubquery(filters)
|
||||
if standardIDs == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
WITH src AS (
|
||||
SELECT DISTINCT pf.production_standard_id, pf.fcr_id
|
||||
FROM project_flocks pf
|
||||
JOIN project_flock_kandangs pfk ON pfk.project_flock_id = pf.id
|
||||
JOIN kandangs k ON k.id = pfk.kandang_id
|
||||
WHERE pf.production_standard_id > 0 AND pf.fcr_id > 0
|
||||
%s
|
||||
),
|
||||
actual AS (
|
||||
SELECT u.week AS week,
|
||||
pf.fcr_id AS fcr_id,
|
||||
AVG((u.chart_data->'statistics'->>'average_weight')::numeric) AS avg_weight
|
||||
FROM project_flock_kandang_uniformity u
|
||||
JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id
|
||||
JOIN project_flocks pf ON pf.id = pfk.project_flock_id
|
||||
JOIN kandangs k ON k.id = pfk.kandang_id
|
||||
WHERE u.week IN ? AND u.uniform_date IS NOT NULL AND pf.fcr_id > 0
|
||||
%s
|
||||
GROUP BY u.week, pf.fcr_id
|
||||
),
|
||||
target AS (
|
||||
SELECT sgd.week AS week,
|
||||
src.fcr_id AS fcr_id,
|
||||
AVG(sgd.target_mean_bw) AS target_mean_bw
|
||||
FROM standard_growth_details sgd
|
||||
JOIN src ON src.production_standard_id = sgd.production_standard_id
|
||||
WHERE sgd.week IN ?
|
||||
GROUP BY sgd.week, src.fcr_id
|
||||
),
|
||||
weights AS (
|
||||
SELECT COALESCE(a.week, t.week) AS week,
|
||||
COALESCE(a.fcr_id, t.fcr_id) AS fcr_id,
|
||||
COALESCE(
|
||||
CASE WHEN a.avg_weight > 10 THEN a.avg_weight / 1000 ELSE a.avg_weight END,
|
||||
CASE WHEN t.target_mean_bw > 10 THEN t.target_mean_bw / 1000 ELSE t.target_mean_bw END
|
||||
) AS weight
|
||||
FROM actual a
|
||||
FULL OUTER JOIN target t ON t.week = a.week AND t.fcr_id = a.fcr_id
|
||||
)
|
||||
SELECT w.week AS week,
|
||||
COALESCE(AVG(
|
||||
COALESCE(
|
||||
(SELECT fs.fcr_number
|
||||
FROM fcr_standards fs
|
||||
WHERE fs.fcr_id = w.fcr_id
|
||||
AND fs.weight >= w.weight
|
||||
ORDER BY fs.weight ASC
|
||||
LIMIT 1),
|
||||
(SELECT fs.fcr_number
|
||||
FROM fcr_standards fs
|
||||
WHERE fs.fcr_id = w.fcr_id
|
||||
ORDER BY fs.weight DESC
|
||||
LIMIT 1)
|
||||
)
|
||||
), 0) AS std_fcr
|
||||
FROM weights w
|
||||
GROUP BY w.week
|
||||
ORDER BY w.week ASC
|
||||
`, filterClause, filterClause)
|
||||
|
||||
args := make([]interface{}, 0, len(filterArgs)*2+2)
|
||||
args = append(args, filterArgs...)
|
||||
args = append(args, weeks)
|
||||
args = append(args, filterArgs...)
|
||||
args = append(args, weeks)
|
||||
|
||||
var rows []StandardWeeklyFcrMetric
|
||||
if err := r.DB().WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil {
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("production_standard_details AS psd").
|
||||
Select("psd.week AS week, COALESCE(AVG(psd.standard_fcr), 0) AS std_fcr").
|
||||
Where("psd.week IN ?", weeks).
|
||||
Where("psd.production_standard_id IN (?)", standardIDs)
|
||||
|
||||
if err := db.Group("psd.week").Order("psd.week ASC").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -510,30 +444,6 @@ func (r *DashboardRepositoryImpl) standardIDSubquery(filters *validation.Dashboa
|
||||
return db
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) standardSourceSubquery(filters *validation.DashboardFilter) *gorm.DB {
|
||||
db := r.DB().
|
||||
Table("project_flocks AS pf").
|
||||
Select("DISTINCT pf.production_standard_id, pf.fcr_id").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.project_flock_id = pf.id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Where("pf.production_standard_id > 0").
|
||||
Where("pf.fcr_id > 0")
|
||||
|
||||
if filters != nil {
|
||||
if len(filters.FlockIds) > 0 {
|
||||
db = db.Where("pf.id IN ?", filters.FlockIds)
|
||||
}
|
||||
if len(filters.KandangIds) > 0 {
|
||||
db = db.Where("k.id IN ?", filters.KandangIds)
|
||||
}
|
||||
if len(filters.LokasiIds) > 0 {
|
||||
db = db.Where("k.location_id IN ?", filters.LokasiIds)
|
||||
}
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (r *DashboardRepositoryImpl) GetComparisonSeries(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter, comparisonType string) ([]ComparisonSeries, error) {
|
||||
seriesExpr, labelExpr, groupExpr, orderExpr, err := comparisonSeriesColumns(comparisonType)
|
||||
if err != nil {
|
||||
@@ -635,13 +545,19 @@ func (r *DashboardRepositoryImpl) GetComparisonWeeklyUniformityMetrics(ctx conte
|
||||
func (r *DashboardRepositoryImpl) GetEggQualityWeeklyMetrics(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]EggQualityWeeklyMetric, error) {
|
||||
var rows []EggQualityWeeklyMetric
|
||||
|
||||
weekExpr := `CASE
|
||||
WHEN r.day IS NULL OR r.day <= 0 THEN 1
|
||||
WHEN UPPER(pf.category) = 'LAYING' THEN ((r.day - 1) / 7 + 1) + 17
|
||||
ELSE ((r.day - 1) / 7 + 1)
|
||||
END`
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("recording_eggs AS re").
|
||||
Select(`
|
||||
((r.day - 1) / 7 + 1) AS week,
|
||||
Select(fmt.Sprintf(`
|
||||
%s AS week,
|
||||
COALESCE(SUM(CASE WHEN f.name = ? THEN re.qty ELSE 0 END), 0) AS normal_qty,
|
||||
COALESCE(SUM(CASE WHEN f.name IN (?, ?, ?) THEN re.qty ELSE 0 END), 0) AS abnormal_qty,
|
||||
COALESCE(SUM(re.qty), 0) AS total_qty`,
|
||||
COALESCE(SUM(re.qty), 0) AS total_qty`, weekExpr),
|
||||
utils.FlagTelurUtuh,
|
||||
utils.FlagTelurPutih,
|
||||
utils.FlagTelurRetak,
|
||||
@@ -650,6 +566,7 @@ func (r *DashboardRepositoryImpl) GetEggQualityWeeklyMetrics(ctx context.Context
|
||||
Joins("JOIN recordings AS r ON r.id = re.recording_id").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
|
||||
Joins("JOIN product_warehouses AS pw ON pw.id = re.product_warehouse_id").
|
||||
Joins("JOIN products AS p ON p.id = pw.product_id").
|
||||
Joins("JOIN flags AS f ON f.flagable_id = p.id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
@@ -670,14 +587,21 @@ func (r *DashboardRepositoryImpl) GetEggQualityWeeklyMetrics(ctx context.Context
|
||||
func (r *DashboardRepositoryImpl) GetEggWeightWeeklyGrams(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyEggWeightMetric, error) {
|
||||
var rows []WeeklyEggWeightMetric
|
||||
|
||||
weekExpr := `CASE
|
||||
WHEN r.day IS NULL OR r.day <= 0 THEN 1
|
||||
WHEN UPPER(pf.category) = 'LAYING' THEN ((r.day - 1) / 7 + 1) + 17
|
||||
ELSE ((r.day - 1) / 7 + 1)
|
||||
END`
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("recording_eggs AS re").
|
||||
Select(`
|
||||
((r.day - 1) / 7 + 1) AS week,
|
||||
COALESCE(SUM(re.weight * 1000), 0) AS egg_weight_grams`).
|
||||
Select(fmt.Sprintf(`
|
||||
%s AS week,
|
||||
COALESCE(SUM(re.weight * 1000), 0) AS egg_weight_grams`, weekExpr)).
|
||||
Joins("JOIN recordings AS r ON r.id = re.recording_id").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
|
||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||
Where("r.deleted_at IS NULL").
|
||||
Where("r.day IS NOT NULL AND r.day > 0")
|
||||
@@ -694,15 +618,22 @@ func (r *DashboardRepositoryImpl) GetEggWeightWeeklyGrams(ctx context.Context, s
|
||||
func (r *DashboardRepositoryImpl) GetFeedUsageWeeklyByUom(ctx context.Context, start, end time.Time, filters *validation.DashboardFilter) ([]WeeklyFeedUsageMetric, error) {
|
||||
var rows []WeeklyFeedUsageMetric
|
||||
|
||||
weekExpr := `CASE
|
||||
WHEN r.day IS NULL OR r.day <= 0 THEN 1
|
||||
WHEN UPPER(pf.category) = 'LAYING' THEN ((r.day - 1) / 7 + 1) + 17
|
||||
ELSE ((r.day - 1) / 7 + 1)
|
||||
END`
|
||||
|
||||
db := r.DB().WithContext(ctx).
|
||||
Table("recording_stocks AS rs").
|
||||
Select(`
|
||||
((r.day - 1) / 7 + 1) AS week,
|
||||
Select(fmt.Sprintf(`
|
||||
%s AS week,
|
||||
COALESCE(SUM(rs.usage_qty), 0) + COALESCE(SUM(rs.pending_qty), 0) AS total_qty,
|
||||
LOWER(uoms.name) AS uom_name`).
|
||||
LOWER(uoms.name) AS uom_name`, weekExpr)).
|
||||
Joins("JOIN recordings AS r ON r.id = rs.recording_id").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id").
|
||||
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
|
||||
Joins("JOIN products AS p ON p.id = pw.product_id").
|
||||
Joins("JOIN uoms ON uoms.id = p.uom_id").
|
||||
|
||||
@@ -24,46 +24,81 @@ func NewTransactionController(transactionService service.TransactionService) *Tr
|
||||
}
|
||||
|
||||
func (u *TransactionController) GetAll(c *fiber.Ctx) error {
|
||||
parseOptionalUint := func(key string) (*uint, error) {
|
||||
parseUintListParam := func(key string) ([]uint, error) {
|
||||
raw := strings.TrimSpace(c.Query(key, ""))
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parsed, err := strconv.ParseUint(raw, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid "+key)
|
||||
parts := strings.Split(raw, ",")
|
||||
ids := make([]uint, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed == "" {
|
||||
return nil, strconv.ErrSyntax
|
||||
}
|
||||
parsed, err := strconv.ParseUint(trimmed, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsed == 0 {
|
||||
continue
|
||||
}
|
||||
ids = append(ids, uint(parsed))
|
||||
}
|
||||
if parsed == 0 {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
value := uint(parsed)
|
||||
return &value, nil
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
bankId, err := parseOptionalUint("bank_id")
|
||||
if err != nil {
|
||||
return err
|
||||
parseStringListParam := func(key string) ([]string, error) {
|
||||
raw := strings.TrimSpace(c.Query(key, ""))
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
values := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed == "" {
|
||||
return nil, strconv.ErrSyntax
|
||||
}
|
||||
values = append(values, trimmed)
|
||||
}
|
||||
if len(values) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
customerId, err := parseOptionalUint("customer_id")
|
||||
|
||||
bankIDs, err := parseUintListParam("bank_ids")
|
||||
if err != nil {
|
||||
return err
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid bank_ids")
|
||||
}
|
||||
supplierId, err := parseOptionalUint("supplier_id")
|
||||
customerIDs, err := parseUintListParam("customer_ids")
|
||||
if err != nil {
|
||||
return err
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid customer_ids")
|
||||
}
|
||||
supplierIDs, err := parseUintListParam("supplier_ids")
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid supplier_ids")
|
||||
}
|
||||
transactionTypes, err := parseStringListParam("transaction_types")
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_types")
|
||||
}
|
||||
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
TransactionType: c.Query("transaction_type", ""),
|
||||
BankId: bankId,
|
||||
CustomerId: customerId,
|
||||
SupplierId: supplierId,
|
||||
SortDate: c.Query("sort_date", ""),
|
||||
StartDate: c.Query("start_date", ""),
|
||||
EndDate: c.Query("end_date", ""),
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
TransactionTypes: transactionTypes,
|
||||
BankIDs: bankIDs,
|
||||
CustomerIDs: customerIDs,
|
||||
SupplierIDs: supplierIDs,
|
||||
SortDate: c.Query("sort_date", ""),
|
||||
StartDate: c.Query("start_date", ""),
|
||||
EndDate: c.Query("end_date", ""),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
|
||||
@@ -74,33 +74,59 @@ func (s transactionService) GetAll(c *fiber.Ctx, params *validation.Query) ([]en
|
||||
|
||||
if params.Search != "" {
|
||||
like := "%" + strings.ToLower(strings.TrimSpace(params.Search)) + "%"
|
||||
db = db.Joins(
|
||||
"LEFT JOIN customers ON customers.id = payments.party_id AND payments.party_type = ? AND customers.deleted_at IS NULL",
|
||||
string(utils.PaymentPartyCustomer),
|
||||
).Joins(
|
||||
"LEFT JOIN suppliers ON suppliers.id = payments.party_id AND payments.party_type = ? AND suppliers.deleted_at IS NULL",
|
||||
string(utils.PaymentPartySupplier),
|
||||
).Joins(
|
||||
"LEFT JOIN banks ON banks.id = payments.bank_id AND banks.deleted_at IS NULL",
|
||||
)
|
||||
db = db.Where(
|
||||
`LOWER(payment_code) LIKE ? OR
|
||||
LOWER(COALESCE(reference_number, '')) LIKE ? OR
|
||||
LOWER(COALESCE(payment_method, '')) LIKE ? OR
|
||||
LOWER(COALESCE(transaction_type, '')) LIKE ? OR
|
||||
LOWER(COALESCE(notes, '')) LIKE ?`,
|
||||
like, like, like, like,
|
||||
LOWER(COALESCE(notes, '')) LIKE ? OR
|
||||
LOWER(COALESCE(customers.name, '')) LIKE ? OR
|
||||
LOWER(COALESCE(suppliers.name, '')) LIKE ? OR
|
||||
LOWER(COALESCE(banks.name, '')) LIKE ?`,
|
||||
like, like, like, like, like, like, like, like,
|
||||
)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(params.TransactionType) != "" {
|
||||
db = db.Where("transaction_type = ?", strings.ToUpper(strings.TrimSpace(params.TransactionType)))
|
||||
if len(params.TransactionTypes) > 0 {
|
||||
types := make([]string, 0, len(params.TransactionTypes))
|
||||
for _, transactionType := range params.TransactionTypes {
|
||||
normalized := strings.ToUpper(strings.TrimSpace(transactionType))
|
||||
if normalized == "" {
|
||||
continue
|
||||
}
|
||||
types = append(types, normalized)
|
||||
}
|
||||
if len(types) > 0 {
|
||||
db = db.Where("transaction_type IN ?", types)
|
||||
}
|
||||
}
|
||||
|
||||
if params.BankId != nil {
|
||||
db = db.Where("bank_id = ?", *params.BankId)
|
||||
if len(params.BankIDs) > 0 {
|
||||
db = db.Where("bank_id IN ?", params.BankIDs)
|
||||
}
|
||||
|
||||
if params.CustomerId != nil && params.SupplierId != nil {
|
||||
customerIDs := params.CustomerIDs
|
||||
supplierIDs := params.SupplierIDs
|
||||
|
||||
if len(customerIDs) > 0 && len(supplierIDs) > 0 {
|
||||
db = db.Where(
|
||||
"(party_type = ? AND party_id = ?) OR (party_type = ? AND party_id = ?)",
|
||||
string(utils.PaymentPartyCustomer), *params.CustomerId,
|
||||
string(utils.PaymentPartySupplier), *params.SupplierId,
|
||||
"(party_type = ? AND party_id IN ?) OR (party_type = ? AND party_id IN ?)",
|
||||
string(utils.PaymentPartyCustomer), customerIDs,
|
||||
string(utils.PaymentPartySupplier), supplierIDs,
|
||||
)
|
||||
} else if params.CustomerId != nil {
|
||||
db = db.Where("party_type = ? AND party_id = ?", string(utils.PaymentPartyCustomer), *params.CustomerId)
|
||||
} else if params.SupplierId != nil {
|
||||
db = db.Where("party_type = ? AND party_id = ?", string(utils.PaymentPartySupplier), *params.SupplierId)
|
||||
} else if len(customerIDs) > 0 {
|
||||
db = db.Where("party_type = ? AND party_id IN ?", string(utils.PaymentPartyCustomer), customerIDs)
|
||||
} else if len(supplierIDs) > 0 {
|
||||
db = db.Where("party_type = ? AND party_id IN ?", string(utils.PaymentPartySupplier), supplierIDs)
|
||||
}
|
||||
|
||||
if startDate != nil {
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
package validation
|
||||
|
||||
type Create struct {
|
||||
Name string `json:"name" validate:"required_strict,min=3"`
|
||||
Name string `json:"name" validate:"required_strict,min=3"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
TransactionType string `query:"transaction_type" validate:"omitempty,max=50"`
|
||||
BankId *uint `query:"bank_id" validate:"omitempty,number,gt=0"`
|
||||
CustomerId *uint `query:"customer_id" validate:"omitempty,number,gt=0"`
|
||||
SupplierId *uint `query:"supplier_id" validate:"omitempty,number,gt=0"`
|
||||
SortDate string `query:"sort_date" validate:"omitempty,oneof=created_at payment_date"`
|
||||
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
TransactionTypes []string `query:"transaction_types" validate:"omitempty,dive,max=50"`
|
||||
BankIDs []uint `query:"bank_ids" validate:"omitempty,dive,gt=0"`
|
||||
CustomerIDs []uint `query:"customer_ids" validate:"omitempty,dive,gt=0"`
|
||||
SupplierIDs []uint `query:"supplier_ids" validate:"omitempty,dive,gt=0"`
|
||||
SortDate string `query:"sort_date" validate:"omitempty,oneof=created_at payment_date"`
|
||||
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
}
|
||||
|
||||
@@ -2,9 +2,13 @@ package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type AdjustmentStockRepository interface {
|
||||
@@ -12,6 +16,7 @@ type AdjustmentStockRepository interface {
|
||||
GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.AdjustmentStock, error)
|
||||
WithTx(tx *gorm.DB) AdjustmentStockRepository
|
||||
DB() *gorm.DB
|
||||
GenerateSequentialNumber(ctx context.Context, prefix string) (string, error)
|
||||
}
|
||||
|
||||
type adjustmentStockRepositoryImpl struct {
|
||||
@@ -50,3 +55,71 @@ func (r *adjustmentStockRepositoryImpl) WithTx(tx *gorm.DB) AdjustmentStockRepos
|
||||
func (r *adjustmentStockRepositoryImpl) DB() *gorm.DB {
|
||||
return r.db
|
||||
}
|
||||
|
||||
func (r *adjustmentStockRepositoryImpl) GenerateSequentialNumber(ctx context.Context, prefix string) (string, error) {
|
||||
var values []string
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&entity.AdjustmentStock{}).
|
||||
Where(fmt.Sprintf("%s ILIKE ?", "adj_number"), prefix+"%").
|
||||
Select("adj_number").
|
||||
Order(fmt.Sprintf("%s DESC", "adj_number")).
|
||||
Limit(20).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Pluck("adj_number", &values).Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
next := 1
|
||||
for _, value := range values {
|
||||
if number, ok := parseNumericSuffix(value, prefix); ok {
|
||||
next = number + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const maxAttempts = 20
|
||||
for attempt := 0; attempt < maxAttempts; attempt++ {
|
||||
candidate := fmt.Sprintf("%s%0*d", prefix, 5, next)
|
||||
exists, err := r.numberExists(ctx, r.db, candidate)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !exists {
|
||||
return candidate, nil
|
||||
}
|
||||
next++
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("unable to generate unique %s", "adj_number")
|
||||
}
|
||||
|
||||
func (r *adjustmentStockRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, value string) (bool, error) {
|
||||
var count int64
|
||||
if err := db.WithContext(ctx).
|
||||
Model(&entity.AdjustmentStock{}).
|
||||
Where(fmt.Sprintf("%s = ?", "adj_number"), value).
|
||||
Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func parseNumericSuffix(value, prefix string) (int, bool) {
|
||||
if !strings.HasPrefix(value, prefix) {
|
||||
return 0, false
|
||||
}
|
||||
suffix := strings.TrimPrefix(value, prefix)
|
||||
if suffix == "" {
|
||||
return 0, false
|
||||
}
|
||||
trimmed := strings.TrimLeft(suffix, "0")
|
||||
if trimmed == "" {
|
||||
trimmed = "0"
|
||||
}
|
||||
number, err := strconv.Atoi(trimmed)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return number, true
|
||||
}
|
||||
|
||||
@@ -200,6 +200,11 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
adjustmentStock := &entity.AdjustmentStock{
|
||||
ProductWarehouseId: productWarehouse.Id,
|
||||
}
|
||||
code, err := s.AdjustmentStockRepository.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
adjustmentStock.AdjNumber = code
|
||||
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
|
||||
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
|
||||
|
||||
@@ -62,6 +62,7 @@ type StockLogDetailDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Increase float64 `json:"increase"`
|
||||
Decrease float64 `json:"decrease"`
|
||||
Stock float64 `json:"stock"`
|
||||
LoggableType string `json:"loggable_type"`
|
||||
LoggableId uint `json:"loggable_id"`
|
||||
Notes *string `json:"notes"`
|
||||
@@ -195,6 +196,7 @@ func mapStockLogs(src []entity.StockLog) []StockLogDetailDTO {
|
||||
Id: log.Id,
|
||||
Increase: log.Increase,
|
||||
Decrease: log.Decrease,
|
||||
Stock: log.Stock,
|
||||
LoggableType: log.LoggableType,
|
||||
LoggableId: log.LoggableId,
|
||||
Notes: notes,
|
||||
|
||||
@@ -175,5 +175,47 @@ func (s productStockService) GetOne(c *fiber.Ctx, id uint) (*entity.Product, err
|
||||
s.Log.Errorf("Failed get product by id: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(product.ProductWarehouses) > 0 {
|
||||
ids := make([]uint, 0, len(product.ProductWarehouses))
|
||||
for _, pw := range product.ProductWarehouses {
|
||||
if pw.Id != 0 {
|
||||
ids = append(ids, pw.Id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ids) > 0 {
|
||||
type pendingUsageRow struct {
|
||||
ProductWarehouseId uint
|
||||
PendingQty float64
|
||||
}
|
||||
|
||||
var rows []pendingUsageRow
|
||||
if err := s.ProductRepository.DB().WithContext(c.Context()).
|
||||
Table("recording_stocks").
|
||||
Select("product_warehouse_id, COALESCE(SUM(pending_qty), 0) AS pending_qty").
|
||||
Where("pending_qty > 0").
|
||||
Where("product_warehouse_id IN ?", ids).
|
||||
Group("product_warehouse_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
s.Log.Errorf("Failed to load pending usage for product warehouses: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(rows) > 0 {
|
||||
pendingMap := make(map[uint]float64, len(rows))
|
||||
for _, row := range rows {
|
||||
pendingMap[row.ProductWarehouseId] = row.PendingQty
|
||||
}
|
||||
|
||||
for i := range product.ProductWarehouses {
|
||||
pw := &product.ProductWarehouses[i]
|
||||
if pending, ok := pendingMap[pw.Id]; ok && pending != 0 {
|
||||
pw.Quantity -= pending
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return product, nil
|
||||
}
|
||||
|
||||
+42
-12
@@ -100,9 +100,13 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
var marketingTypes []string
|
||||
if params.Type != "" {
|
||||
if !utils.IsValidMarketingType(params.Type) {
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing type")
|
||||
marketingTypes = utils.ParseQueryArray(params.Type)
|
||||
for _, t := range marketingTypes {
|
||||
if !utils.IsValidMarketingType(t) {
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,16 +139,42 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
||||
db = db.Where("warehouse_id = ?", params.WarehouseId)
|
||||
}
|
||||
|
||||
if params.Type != "" {
|
||||
switch params.Type {
|
||||
case string(utils.MarketingTypeAyamPullet):
|
||||
db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer)})
|
||||
case string(utils.MarketingTypeAyam):
|
||||
db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagAyamMati)})
|
||||
case string(utils.MarketingTypeTelur):
|
||||
db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagTelur), string(utils.FlagTelurUtuh), string(utils.FlagTelurPecah), string(utils.FlagTelurPutih), string(utils.FlagTelurRetak)})
|
||||
case string(utils.MarketingTypeTrading):
|
||||
db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher), string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia), string(utils.FlagEkspedisi)})
|
||||
if len(marketingTypes) > 0 {
|
||||
flagSet := make(map[string]struct{})
|
||||
for _, t := range marketingTypes {
|
||||
switch t {
|
||||
case string(utils.MarketingTypeAyamPullet):
|
||||
flagSet[string(utils.FlagDOC)] = struct{}{}
|
||||
flagSet[string(utils.FlagPullet)] = struct{}{}
|
||||
flagSet[string(utils.FlagLayer)] = struct{}{}
|
||||
case string(utils.MarketingTypeAyam):
|
||||
flagSet[string(utils.FlagAyamAfkir)] = struct{}{}
|
||||
flagSet[string(utils.FlagAyamCulling)] = struct{}{}
|
||||
flagSet[string(utils.FlagAyamMati)] = struct{}{}
|
||||
case string(utils.MarketingTypeTelur):
|
||||
flagSet[string(utils.FlagTelur)] = struct{}{}
|
||||
flagSet[string(utils.FlagTelurUtuh)] = struct{}{}
|
||||
flagSet[string(utils.FlagTelurPecah)] = struct{}{}
|
||||
flagSet[string(utils.FlagTelurPutih)] = struct{}{}
|
||||
flagSet[string(utils.FlagTelurRetak)] = struct{}{}
|
||||
case string(utils.MarketingTypeTrading):
|
||||
flagSet[string(utils.FlagPakan)] = struct{}{}
|
||||
flagSet[string(utils.FlagPreStarter)] = struct{}{}
|
||||
flagSet[string(utils.FlagStarter)] = struct{}{}
|
||||
flagSet[string(utils.FlagFinisher)] = struct{}{}
|
||||
flagSet[string(utils.FlagOVK)] = struct{}{}
|
||||
flagSet[string(utils.FlagObat)] = struct{}{}
|
||||
flagSet[string(utils.FlagVitamin)] = struct{}{}
|
||||
flagSet[string(utils.FlagKimia)] = struct{}{}
|
||||
flagSet[string(utils.FlagEkspedisi)] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(flagSet) > 0 {
|
||||
flags := make([]string, 0, len(flagSet))
|
||||
for f := range flagSet {
|
||||
flags = append(flags, f)
|
||||
}
|
||||
db = s.Repository.ApplyFlagsFilter(db, flags)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -20,5 +20,5 @@ type Query struct {
|
||||
Flags string `query:"flags" validate:"omitempty"`
|
||||
KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"`
|
||||
TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"`
|
||||
Type string `query:"type" validate:"omitempty,oneof=AYAM TELUR TRADING AYAM_PULLET"`
|
||||
Type string `query:"type" validate:"omitempty"`
|
||||
}
|
||||
|
||||
@@ -483,7 +483,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
if len(stockLogs) > 0 {
|
||||
latestStockLog := stockLogs[0]
|
||||
stockLogDecrease.Stock -= latestStockLog.Stock - stockLogDecrease.Decrease
|
||||
stockLogDecrease.Stock = latestStockLog.Stock - stockLogDecrease.Decrease
|
||||
} else {
|
||||
stockLogDecrease.Stock -= stockLogDecrease.Decrease
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package controller
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||
@@ -23,9 +24,38 @@ func NewDeliveryOrdersController(deliveryOrdersService service.DeliveryOrdersSer
|
||||
}
|
||||
|
||||
func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error {
|
||||
parseUintListParam := func(param string) ([]uint, error) {
|
||||
if param == "" {
|
||||
return nil, nil
|
||||
}
|
||||
parts := strings.Split(param, ",")
|
||||
ids := make([]uint, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed == "" {
|
||||
return nil, strconv.ErrSyntax
|
||||
}
|
||||
parsed, err := strconv.ParseUint(trimmed, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = append(ids, uint(parsed))
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
productIDs, err := parseUintListParam(c.Query("product_ids", ""))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid product_ids")
|
||||
}
|
||||
|
||||
query := &validation.DeliveryOrderQuery{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: strings.TrimSpace(c.Query("search", "")),
|
||||
ProductIDs: productIDs,
|
||||
Status: strings.ReplaceAll(strings.TrimSpace(c.Query("status", "")), "_", " "),
|
||||
CustomerId: uint(c.QueryInt("customer_id", 0)),
|
||||
MarketingId: uint(c.QueryInt("marketing_id", 0)),
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
type MarketingRelationDTO struct {
|
||||
Id uint `json:"id"`
|
||||
SoNumber string `json:"so_number"`
|
||||
DoNumber *string `json:"do_number"`
|
||||
SoDate time.Time `json:"so_date"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
}
|
||||
@@ -95,9 +97,16 @@ type DeliveryMarketingProductDTO struct {
|
||||
}
|
||||
|
||||
func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO {
|
||||
var doNumber *string
|
||||
if doNumbers := collectDoNumbers(marketing); len(doNumbers) > 0 {
|
||||
value := doNumbers[0]
|
||||
doNumber = &value
|
||||
}
|
||||
|
||||
return MarketingRelationDTO{
|
||||
Id: marketing.Id,
|
||||
SoNumber: marketing.SoNumber,
|
||||
DoNumber: doNumber,
|
||||
SoDate: marketing.SoDate,
|
||||
Notes: marketing.Notes,
|
||||
}
|
||||
@@ -182,7 +191,6 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M
|
||||
salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product, marketing.MarketingType)
|
||||
}
|
||||
}
|
||||
|
||||
return MarketingListDTO{
|
||||
MarketingRelationDTO: ToMarketingRelationDTO(marketing),
|
||||
Customer: customer,
|
||||
@@ -239,7 +247,6 @@ func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity
|
||||
mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval)
|
||||
latestApproval = mapped
|
||||
}
|
||||
|
||||
return MarketingDetailDTO{
|
||||
MarketingRelationDTO: ToMarketingRelationDTO(marketing),
|
||||
SoDocs: marketing.SoDocs,
|
||||
@@ -346,11 +353,46 @@ func groupDeliveryProducts(products []MarketingDeliveryProductDTO, soNumber stri
|
||||
}
|
||||
|
||||
func GenerateDeliveryOrderNumber(soNumber string, deliveryDate *time.Time, warehouseId uint) string {
|
||||
dateStr := ""
|
||||
if deliveryDate != nil {
|
||||
dateStr = deliveryDate.Format("20060102")
|
||||
numberPrefix := soNumber
|
||||
if strings.HasPrefix(strings.ToUpper(strings.TrimSpace(soNumber)), "SO-") {
|
||||
numberPrefix = "DO-" + soNumber[3:]
|
||||
}
|
||||
return fmt.Sprintf("%s-%s-%d", soNumber, dateStr, warehouseId)
|
||||
return numberPrefix
|
||||
}
|
||||
|
||||
func collectDoNumbers(marketing *entity.Marketing) []string {
|
||||
if marketing == nil || len(marketing.Products) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
for _, product := range marketing.Products {
|
||||
if product.DeliveryProduct == nil || product.DeliveryProduct.DeliveryDate == nil {
|
||||
continue
|
||||
}
|
||||
warehouseID := product.ProductWarehouse.WarehouseId
|
||||
if warehouseID == 0 && product.ProductWarehouse.Warehouse.Id != 0 {
|
||||
warehouseID = product.ProductWarehouse.Warehouse.Id
|
||||
}
|
||||
if warehouseID == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
doNumber := GenerateDeliveryOrderNumber(marketing.SoNumber, product.DeliveryProduct.DeliveryDate, warehouseID)
|
||||
if doNumber != "" {
|
||||
seen[doNumber] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if len(seen) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, 0, len(seen))
|
||||
for value := range seen {
|
||||
result = append(result, value)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func getVehicleNumber(e entity.MarketingProduct) string {
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -116,6 +116,83 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
|
||||
Preload("Products.ProductWarehouse.Warehouse").
|
||||
Preload("Products.DeliveryProduct")
|
||||
|
||||
if params.Status != "" {
|
||||
status := strings.TrimSpace(params.Status)
|
||||
latestApprovalSubQuery := s.MarketingRepo.DB().
|
||||
WithContext(c.Context()).
|
||||
Table("approvals").
|
||||
Select("DISTINCT ON (approvable_id) approvable_id, step_name, action").
|
||||
Where("approvable_type = ?", utils.ApprovalWorkflowMarketing.String()).
|
||||
Order("approvable_id, id DESC")
|
||||
|
||||
if strings.EqualFold(status, "DITOLAK") {
|
||||
db = db.Where(`EXISTS (
|
||||
SELECT 1
|
||||
FROM (?) AS latest_approval
|
||||
WHERE latest_approval.approvable_id = marketings.id
|
||||
AND latest_approval.action = ?
|
||||
)`, latestApprovalSubQuery, string(entity.ApprovalActionRejected))
|
||||
} else {
|
||||
db = db.Where(`EXISTS (
|
||||
SELECT 1
|
||||
FROM (?) AS latest_approval
|
||||
WHERE latest_approval.approvable_id = marketings.id
|
||||
AND LOWER(latest_approval.step_name) = LOWER(?)
|
||||
AND (latest_approval.action IS NULL OR latest_approval.action <> ?)
|
||||
)`, latestApprovalSubQuery, status, string(entity.ApprovalActionRejected))
|
||||
}
|
||||
}
|
||||
|
||||
if params.Search != "" {
|
||||
searchPattern := "%" + params.Search + "%"
|
||||
db = db.Where(`(
|
||||
marketings.so_number ILIKE ? OR
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM customers c
|
||||
WHERE c.id = marketings.customer_id
|
||||
AND c.name ILIKE ?
|
||||
) OR
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM users su
|
||||
WHERE su.id = marketings.sales_person_id
|
||||
AND su.name ILIKE ?
|
||||
) OR
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM marketing_products mp
|
||||
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
||||
JOIN products p ON p.id = pw.product_id
|
||||
WHERE mp.marketing_id = marketings.id
|
||||
AND p.name ILIKE ?
|
||||
) OR
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM marketing_products mp
|
||||
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
||||
JOIN warehouses w ON w.id = pw.warehouse_id
|
||||
WHERE mp.marketing_id = marketings.id
|
||||
AND w.name ILIKE ?
|
||||
)
|
||||
)`, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
if len(params.ProductIDs) > 0 {
|
||||
db = db.Where(`EXISTS (
|
||||
SELECT 1
|
||||
FROM marketing_products mp
|
||||
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
||||
JOIN products p ON p.id = pw.product_id
|
||||
WHERE mp.marketing_id = marketings.id
|
||||
AND p.id IN ?
|
||||
)`, params.ProductIDs)
|
||||
}
|
||||
|
||||
if params.CustomerId != 0 {
|
||||
db = db.Where("marketings.customer_id = ?", params.CustomerId)
|
||||
}
|
||||
|
||||
if scope.Restrict {
|
||||
if len(scope.IDs) == 0 {
|
||||
return db.Where("1 = 0")
|
||||
|
||||
@@ -152,6 +152,31 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
|
||||
}
|
||||
}
|
||||
|
||||
requestedByWarehouse := make(map[uint]float64)
|
||||
for _, item := range req.MarketingProducts {
|
||||
if item.ProductWarehouseId == 0 {
|
||||
continue
|
||||
}
|
||||
requestedByWarehouse[item.ProductWarehouseId] += item.Qty
|
||||
}
|
||||
|
||||
for pwID, requestedQty := range requestedByWarehouse {
|
||||
productWarehouse, err := s.ProductWarehouseRepo.GetDetailByID(c.Context(), pwID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", pwID))
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock availability")
|
||||
}
|
||||
availableQty := productWarehouse.Quantity
|
||||
if availableQty+1e-6 < requestedQty {
|
||||
return nil, fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Stok tidak mencukupi untuk gudang %d: diminta %.3f, tersedia %.3f", pwID, requestedQty, availableQty),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
soDate, err := utils.ParseDateString(req.Date)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format")
|
||||
|
||||
@@ -19,9 +19,13 @@ type DeliveryOrderUpdate struct {
|
||||
}
|
||||
|
||||
type DeliveryOrderQuery struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||
MarketingId uint `query:"marketing_id" validate:"omitempty,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"`
|
||||
Search string `query:"search" validate:"omitempty,max=100"`
|
||||
ProductIDs []uint `query:"product_ids" validate:"omitempty,dive,gt=0"`
|
||||
Status string `query:"status" validate:"omitempty,max=50"`
|
||||
CustomerId uint `query:"customer_id" validate:"omitempty,gt=0"`
|
||||
MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"`
|
||||
}
|
||||
|
||||
type DeliveryOrderApprove struct {
|
||||
|
||||
@@ -52,7 +52,22 @@ func (s productCategoryService) GetAll(c *fiber.Ctx, params *validation.Query) (
|
||||
productCategories, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
if params.Search != "" {
|
||||
return db.Where("name ILIKE ?", "%"+params.Search+"%")
|
||||
terms := splitSearchTerms(params.Search)
|
||||
if len(terms) == 0 {
|
||||
return db
|
||||
}
|
||||
if len(terms) == 1 {
|
||||
return db.Where("name ILIKE ?", "%"+terms[0]+"%")
|
||||
}
|
||||
for i, term := range terms {
|
||||
like := "%" + term + "%"
|
||||
if i == 0 {
|
||||
db = db.Where("name ILIKE ?", like)
|
||||
} else {
|
||||
db = db.Or("name ILIKE ?", like)
|
||||
}
|
||||
}
|
||||
return db
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
@@ -64,6 +79,20 @@ func (s productCategoryService) GetAll(c *fiber.Ctx, params *validation.Query) (
|
||||
return productCategories, total, nil
|
||||
}
|
||||
|
||||
func splitSearchTerms(raw string) []string {
|
||||
parts := strings.FieldsFunc(raw, func(r rune) bool {
|
||||
return r == ',' || r == ';' || r == '|'
|
||||
})
|
||||
terms := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed != "" {
|
||||
terms = append(terms, trimmed)
|
||||
}
|
||||
}
|
||||
return terms
|
||||
}
|
||||
|
||||
func (s productCategoryService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductCategory, error) {
|
||||
productCategory, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
areaRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
|
||||
fcrRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
|
||||
flockRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
|
||||
kandangRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||
locationRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
||||
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||
userRelationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
)
|
||||
|
||||
// === DTO Structs (ordered) ===
|
||||
@@ -40,7 +40,7 @@ type ProjectFlockDTO struct {
|
||||
Category string `json:"category"`
|
||||
Flock *flockRelationDTO.FlockRelationDTO `json:"flock"`
|
||||
Area *areaRelationDTO.AreaRelationDTO `json:"area"`
|
||||
Fcr *fcrRelationDTO.FcrRelationDTO `json:"fcr"`
|
||||
StandardFcr *float64 `json:"standard_fcr"`
|
||||
Location *locationRelationDTO.LocationRelationDTO `json:"location"`
|
||||
}
|
||||
|
||||
@@ -97,10 +97,6 @@ func ToAreaDTO(e entity.Area) areaRelationDTO.AreaRelationDTO {
|
||||
return areaRelationDTO.ToAreaRelationDTO(e)
|
||||
}
|
||||
|
||||
func ToFcrDTO(e entity.Fcr) fcrRelationDTO.FcrRelationDTO {
|
||||
return fcrRelationDTO.ToFcrRelationDTO(e)
|
||||
}
|
||||
|
||||
func ToLocationDTO(e entity.Location) locationRelationDTO.LocationRelationDTO {
|
||||
return locationRelationDTO.ToLocationRelationDTO(e)
|
||||
}
|
||||
@@ -121,11 +117,6 @@ func ToProjectFlockDTO(pfk entity.ProjectFlockKandang) ProjectFlockDTO {
|
||||
mapped := areaRelationDTO.ToAreaRelationDTO(e.Area)
|
||||
area = &mapped
|
||||
}
|
||||
var fcr *fcrRelationDTO.FcrRelationDTO
|
||||
if e.Fcr.Id != 0 {
|
||||
mapped := fcrRelationDTO.ToFcrRelationDTO(e.Fcr)
|
||||
fcr = &mapped
|
||||
}
|
||||
var location *locationRelationDTO.LocationRelationDTO
|
||||
if e.Location.Id != 0 {
|
||||
mapped := locationRelationDTO.ToLocationRelationDTO(e.Location)
|
||||
@@ -137,7 +128,7 @@ func ToProjectFlockDTO(pfk entity.ProjectFlockKandang) ProjectFlockDTO {
|
||||
Category: e.Category,
|
||||
Flock: flock,
|
||||
Area: area,
|
||||
Fcr: fcr,
|
||||
StandardFcr: resolveProjectFlockStandardFcr(e),
|
||||
Location: location,
|
||||
}
|
||||
}
|
||||
@@ -222,6 +213,22 @@ func ToChickinListDTOs(e []entity.ProjectChickin) []ChickinListDTO {
|
||||
return result
|
||||
}
|
||||
|
||||
func resolveProjectFlockStandardFcr(e entity.ProjectFlock) *float64 {
|
||||
if e.ProductionStandard.Id == 0 || len(e.ProductionStandard.ProductionStandardDetails) == 0 {
|
||||
return nil
|
||||
}
|
||||
week := 1
|
||||
if e.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||
week = 18
|
||||
}
|
||||
for _, detail := range e.ProductionStandard.ProductionStandardDetails {
|
||||
if detail.Week == week && detail.StandardFCR != nil {
|
||||
return detail.StandardFCR
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO {
|
||||
result := make([]ChickinSimpleDTO, len(e))
|
||||
for i, r := range e {
|
||||
|
||||
@@ -36,6 +36,7 @@ type ChickinService interface {
|
||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error)
|
||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error)
|
||||
EnsureChickInExists(ctx context.Context, projectFlockKandangID uint) error
|
||||
}
|
||||
|
||||
type chickinService struct {
|
||||
@@ -81,7 +82,7 @@ func (s chickinService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
Preload("ProjectFlockKandang.Kandang.Pic").
|
||||
Preload("ProjectFlockKandang.ProjectFlock").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.Area").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.Fcr").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard.ProductionStandardDetails").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.Location").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.Location.Area")
|
||||
|
||||
@@ -731,6 +732,30 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s chickinService) EnsureChickInExists(ctx context.Context, projectFlockKandangID uint) error {
|
||||
if projectFlockKandangID == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
|
||||
}
|
||||
|
||||
populations, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to check project flock population for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memeriksa data chick in")
|
||||
}
|
||||
|
||||
if len(populations) == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock belum memiliki chick in yang disetujui sehingga belum dapat membuat recording")
|
||||
}
|
||||
|
||||
for _, population := range populations {
|
||||
if population.TotalQty > 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording")
|
||||
}
|
||||
|
||||
func (s *chickinService) adjustProductWarehouseQuantities(ctx context.Context, tx *gorm.DB, deltas map[uint]float64) error {
|
||||
if len(deltas) == 0 {
|
||||
return nil
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||
productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto"
|
||||
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
|
||||
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
|
||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||
productionStandardDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/dto"
|
||||
@@ -31,7 +30,7 @@ type ProjectFlockDTO struct {
|
||||
projectFlockDTO.ProjectFlockRelationDTO
|
||||
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
|
||||
Category string `json:"category"`
|
||||
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"`
|
||||
StandardFcr *float64 `json:"standard_fcr,omitempty"`
|
||||
ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"`
|
||||
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||
@@ -86,7 +85,7 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO
|
||||
ProjectFlockRelationDTO: pf.ProjectFlockRelationDTO,
|
||||
Area: pf.Area,
|
||||
Category: pf.Category,
|
||||
Fcr: pf.Fcr,
|
||||
StandardFcr: pf.StandardFcr,
|
||||
ProductionStandard: pf.ProductionStandard,
|
||||
Location: pf.Location,
|
||||
CreatedUser: pf.CreatedUser,
|
||||
|
||||
+16
-18
@@ -308,25 +308,23 @@ func (s projectFlockKandangService) CheckClosing(c *fiber.Ctx, id uint) (*Closin
|
||||
}
|
||||
|
||||
for _, pw := range productWarehouses {
|
||||
if pw.Quantity > 0 {
|
||||
category := ""
|
||||
if pw.Product.ProductCategory.Id != 0 {
|
||||
category = pw.Product.ProductCategory.Name
|
||||
}
|
||||
uomName := ""
|
||||
if pw.Product.Uom.Id != 0 {
|
||||
uomName = pw.Product.Uom.Name
|
||||
}
|
||||
stockRemain = append(stockRemain, StockRemainingDetail{
|
||||
FlagName: string(flagName),
|
||||
ProductWarehouseId: pw.Id,
|
||||
ProductId: pw.ProductId,
|
||||
ProductName: pw.Product.Name,
|
||||
ProductCategory: category,
|
||||
Uom: uomName,
|
||||
Quantity: pw.Quantity,
|
||||
})
|
||||
category := ""
|
||||
if pw.Product.ProductCategory.Id != 0 {
|
||||
category = pw.Product.ProductCategory.Name
|
||||
}
|
||||
uomName := ""
|
||||
if pw.Product.Uom.Id != 0 {
|
||||
uomName = pw.Product.Uom.Name
|
||||
}
|
||||
stockRemain = append(stockRemain, StockRemainingDetail{
|
||||
FlagName: string(flagName),
|
||||
ProductWarehouseId: pw.Id,
|
||||
ProductId: pw.ProductId,
|
||||
ProductName: pw.Product.Name,
|
||||
ProductCategory: category,
|
||||
Uom: uomName,
|
||||
Quantity: pw.Quantity,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
|
||||
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
|
||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
|
||||
@@ -28,7 +27,7 @@ type ProjectFlockListDTO struct {
|
||||
ProjectFlockRelationDTO
|
||||
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
|
||||
Category string `json:"category"`
|
||||
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"`
|
||||
StandardFcr *float64 `json:"standard_fcr,omitempty"`
|
||||
ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"`
|
||||
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
||||
Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"`
|
||||
@@ -99,12 +98,6 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF
|
||||
areaSummary = &mapped
|
||||
}
|
||||
|
||||
var fcrSummary *fcrDTO.FcrRelationDTO
|
||||
if e.Fcr.Id != 0 {
|
||||
mapped := fcrDTO.ToFcrRelationDTO(e.Fcr)
|
||||
fcrSummary = &mapped
|
||||
}
|
||||
|
||||
var productionStandardSummary *productionStandardDTO.ProductionStandardRelationDTO
|
||||
if e.ProductionStandard.Id != 0 {
|
||||
mapped := productionStandardDTO.ToProductionStandardRelationDTO(e.ProductionStandard)
|
||||
@@ -129,7 +122,7 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF
|
||||
Kandangs: kandangSummaries,
|
||||
ProjectBudgets: ToProjectBudgetDTOs(e.Budgets),
|
||||
Category: e.Category,
|
||||
Fcr: fcrSummary,
|
||||
StandardFcr: resolveProjectFlockStandardFcr(e),
|
||||
ProductionStandard: productionStandardSummary,
|
||||
Location: locationSummary,
|
||||
CreatedAt: e.CreatedAt,
|
||||
@@ -204,6 +197,22 @@ func createProjectFlockRelationDTO(e entity.ProjectFlock, period int) ProjectFlo
|
||||
}
|
||||
}
|
||||
|
||||
func resolveProjectFlockStandardFcr(e entity.ProjectFlock) *float64 {
|
||||
if e.ProductionStandard.Id == 0 || len(e.ProductionStandard.ProductionStandardDetails) == 0 {
|
||||
return nil
|
||||
}
|
||||
week := 1
|
||||
if e.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||
week = 18
|
||||
}
|
||||
for _, detail := range e.ProductionStandard.ProductionStandardDetails {
|
||||
if detail.Week == week && detail.StandardFCR != nil {
|
||||
return detail.StandardFCR
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ToProjectBudgetDTO(e entity.ProjectBudget) ProjectBudgetDTO {
|
||||
var nonstockRef *nonstockDTO.NonstockRelationDTO
|
||||
if e.Nonstock != nil && e.Nonstock.Id != 0 {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
|
||||
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
|
||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||
productionStandardDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/dto"
|
||||
@@ -22,7 +21,7 @@ type ProjectFlockWithPivotDTO struct {
|
||||
ProjectFlockRelationDTO
|
||||
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
|
||||
Category string `json:"category"`
|
||||
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"`
|
||||
StandardFcr *float64 `json:"standard_fcr,omitempty"`
|
||||
ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"`
|
||||
ProductionStandardId uint `json:"production_standard_id"`
|
||||
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
||||
@@ -67,10 +66,6 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
|
||||
mapped := areaDTO.ToAreaRelationDTO(e.ProjectFlock.Area)
|
||||
pfLocal.Area = &mapped
|
||||
}
|
||||
if e.ProjectFlock.Fcr.Id != 0 {
|
||||
mapped := fcrDTO.ToFcrRelationDTO(e.ProjectFlock.Fcr)
|
||||
pfLocal.Fcr = &mapped
|
||||
}
|
||||
if e.ProjectFlock.ProductionStandard.Id != 0 {
|
||||
mapped := productionStandardDTO.ToProductionStandardRelationDTO(e.ProjectFlock.ProductionStandard)
|
||||
pfLocal.ProductionStandard = &mapped
|
||||
@@ -83,6 +78,7 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
|
||||
mapped := userDTO.ToUserRelationDTO(e.ProjectFlock.CreatedUser)
|
||||
pfLocal.CreatedUser = &mapped
|
||||
}
|
||||
pfLocal.StandardFcr = resolveProjectFlockStandardFcr(e.ProjectFlock)
|
||||
|
||||
for _, k := range e.ProjectFlock.Kandangs {
|
||||
kb := kandangDTO.ToKandangRelationDTO(k)
|
||||
|
||||
@@ -23,7 +23,6 @@ type ProjectflockRepository interface {
|
||||
GetActiveByLocationID(ctx context.Context, locationID uint64) ([]entity.ProjectFlock, error)
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
AreaExists(ctx context.Context, id uint) (bool, error)
|
||||
FcrExists(ctx context.Context, id uint) (bool, error)
|
||||
ProductionStandardExists(ctx context.Context, id uint) (bool, error)
|
||||
LocationExists(ctx context.Context, id uint) (bool, error)
|
||||
}
|
||||
@@ -67,8 +66,8 @@ func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm
|
||||
return db.
|
||||
Preload("CreatedUser").
|
||||
Preload("Area").
|
||||
Preload("Fcr").
|
||||
Preload("ProductionStandard").
|
||||
Preload("ProductionStandard.ProductionStandardDetails").
|
||||
Preload("Location").
|
||||
Preload("Kandangs").
|
||||
Preload("KandangHistory").
|
||||
@@ -134,14 +133,12 @@ func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch s
|
||||
likeQuery := "%" + normalized + "%"
|
||||
return db.
|
||||
Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
|
||||
Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id").
|
||||
Joins("LEFT JOIN production_standards ON production_standards.id = project_flocks.production_standard_id").
|
||||
Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id").
|
||||
Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
|
||||
Where(`
|
||||
LOWER(areas.name) LIKE ?
|
||||
OR LOWER(project_flocks.category) LIKE ?
|
||||
OR LOWER(fcrs.name) LIKE ?
|
||||
OR LOWER(production_standards.name) LIKE ?
|
||||
OR LOWER(locations.name) LIKE ?
|
||||
OR LOWER(locations.address) LIKE ?
|
||||
@@ -172,7 +169,6 @@ func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch s
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -184,10 +180,6 @@ func (r *ProjectflockRepositoryImpl) AreaExists(ctx context.Context, id uint) (b
|
||||
return repository.Exists[entity.Area](ctx, r.DB(), id)
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) FcrExists(ctx context.Context, id uint) (bool, error) {
|
||||
return repository.Exists[entity.Fcr](ctx, r.DB(), id)
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) ProductionStandardExists(ctx context.Context, id uint) (bool, error) {
|
||||
return repository.Exists[entity.ProductionStandard](ctx, r.DB(), id)
|
||||
}
|
||||
|
||||
+16
-4
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
type ProjectFlockKandangRepository interface {
|
||||
GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error)
|
||||
GetByIDLight(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error)
|
||||
GetByProjectFlockAndKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error)
|
||||
GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error)
|
||||
UpdateClosedAt(ctx context.Context, id uint, t *time.Time) error
|
||||
@@ -117,10 +118,10 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Contex
|
||||
Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\"").
|
||||
Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\"").
|
||||
Preload("ProjectFlock").
|
||||
Preload("ProjectFlock.Fcr").
|
||||
Preload("ProjectFlock.Area").
|
||||
Preload("ProjectFlock.Location").
|
||||
Preload("ProjectFlock.CreatedUser").
|
||||
Preload("ProjectFlock.ProductionStandard.ProductionStandardDetails").
|
||||
Preload("ProjectFlock.Kandangs").
|
||||
Preload("ProjectFlock.KandangHistory").
|
||||
Preload("Kandang").
|
||||
@@ -208,10 +209,10 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFiltersScoped(ctx context.
|
||||
Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\"").
|
||||
Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\"").
|
||||
Preload("ProjectFlock").
|
||||
Preload("ProjectFlock.Fcr").
|
||||
Preload("ProjectFlock.Area").
|
||||
Preload("ProjectFlock.Location").
|
||||
Preload("ProjectFlock.CreatedUser").
|
||||
Preload("ProjectFlock.ProductionStandard.ProductionStandardDetails").
|
||||
Preload("ProjectFlock.Kandangs").
|
||||
Preload("ProjectFlock.KandangHistory").
|
||||
Preload("Kandang").
|
||||
@@ -324,10 +325,10 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint
|
||||
record := new(entity.ProjectFlockKandang)
|
||||
if err := r.db.WithContext(ctx).
|
||||
Preload("ProjectFlock").
|
||||
Preload("ProjectFlock.Fcr").
|
||||
Preload("ProjectFlock.Area").
|
||||
Preload("ProjectFlock.Location").
|
||||
Preload("ProjectFlock.CreatedUser").
|
||||
Preload("ProjectFlock.ProductionStandard.ProductionStandardDetails").
|
||||
Preload("ProjectFlock.Kandangs").
|
||||
Preload("ProjectFlock.KandangHistory").
|
||||
Preload("Kandang").
|
||||
@@ -342,15 +343,26 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint
|
||||
return record, nil
|
||||
}
|
||||
|
||||
// GetByIDLight loads only the minimal relations needed for recording flows.
|
||||
func (r *projectFlockKandangRepositoryImpl) GetByIDLight(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) {
|
||||
record := new(entity.ProjectFlockKandang)
|
||||
if err := r.db.WithContext(ctx).
|
||||
Preload("ProjectFlock").
|
||||
First(record, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error) {
|
||||
record := new(entity.ProjectFlockKandang)
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
|
||||
Preload("ProjectFlock").
|
||||
Preload("ProjectFlock.Fcr").
|
||||
Preload("ProjectFlock.Area").
|
||||
Preload("ProjectFlock.Location").
|
||||
Preload("ProjectFlock.CreatedUser").
|
||||
Preload("ProjectFlock.ProductionStandard.ProductionStandardDetails").
|
||||
Preload("ProjectFlock.Kandangs").
|
||||
Preload("ProjectFlock.KandangHistory").
|
||||
Preload("Kandang").
|
||||
|
||||
@@ -48,6 +48,7 @@ type ProjectflockService interface {
|
||||
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
|
||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
||||
Resubmit(ctx *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error)
|
||||
EnsureProjectFlockApproved(ctx context.Context, projectFlockID uint) error
|
||||
}
|
||||
|
||||
type projectflockService struct {
|
||||
@@ -112,6 +113,32 @@ func (s projectflockService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
|
||||
}
|
||||
}
|
||||
|
||||
func (s projectflockService) EnsureProjectFlockApproved(ctx context.Context, projectFlockID uint) error {
|
||||
if projectFlockID == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
|
||||
}
|
||||
|
||||
approvalSvc := s.ApprovalSvc
|
||||
if approvalSvc == nil {
|
||||
approvalSvc = commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB()))
|
||||
}
|
||||
|
||||
latest, err := approvalSvc.LatestByTarget(ctx, s.approvalWorkflow, projectFlockID, nil)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to check project flock %d approval status: %+v", projectFlockID, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memeriksa status project flock")
|
||||
}
|
||||
|
||||
if latest == nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock masih dalam status pengajuan sehingga belum dapat membuat recording")
|
||||
}
|
||||
if latest.StepNumber != uint16(utils.ProjectFlockStepAktif) || latest.Action == nil || *latest.Action != entity.ApprovalActionApproved {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock masih dalam status pengajuan sehingga belum dapat membuat recording")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, nil, err
|
||||
@@ -282,7 +309,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
||||
|
||||
if err := commonSvc.EnsureRelations(c.Context(),
|
||||
commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists},
|
||||
commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: s.Repository.FcrExists},
|
||||
commonSvc.RelationCheck{Name: "Production Standard", ID: &req.ProductionStandardId, Exists: s.Repository.ProductionStandardExists},
|
||||
commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists},
|
||||
); err != nil {
|
||||
@@ -334,7 +360,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
||||
createBody := &entity.ProjectFlock{
|
||||
AreaId: req.AreaId,
|
||||
Category: cat,
|
||||
FcrId: req.FcrId,
|
||||
ProductionStandardId: req.ProductionStandardId,
|
||||
LocationId: req.LocationId,
|
||||
CreatedBy: actorID,
|
||||
@@ -461,6 +486,15 @@ func (s projectflockService) GetProjectFlockKandangPopulation(ctx *fiber.Ctx, pr
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
|
||||
}
|
||||
|
||||
total, err := s.PopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch project flock kandang population %d: %+v", projectFlockKandangID, err)
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population")
|
||||
}
|
||||
if total > 0 {
|
||||
return total, nil
|
||||
}
|
||||
|
||||
if s.RecordingRepo != nil {
|
||||
latest, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
|
||||
if err != nil {
|
||||
@@ -472,12 +506,6 @@ func (s projectflockService) GetProjectFlockKandangPopulation(ctx *fiber.Ctx, pr
|
||||
}
|
||||
}
|
||||
|
||||
total, err := s.PopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch project flock kandang population %d: %+v", projectFlockKandangID, err)
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population")
|
||||
}
|
||||
|
||||
return total, nil
|
||||
}
|
||||
|
||||
@@ -552,21 +580,22 @@ func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idSt
|
||||
}
|
||||
|
||||
func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) {
|
||||
if s.PopulationRepo == nil {
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured")
|
||||
}
|
||||
|
||||
wh, err := s.WarehouseRepo.GetByKandangID(ctx.Context(), kandangID)
|
||||
pfk, err := s.PivotRepo.GetActiveByKandangID(ctx.Context(), kandangID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), "DOC", wh.Id)
|
||||
total, err := s.PopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), pfk.Id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
total := 0.0
|
||||
for _, pw := range productWarehouses {
|
||||
total += pw.Quantity
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ type Create struct {
|
||||
FlockName string `json:"flock_name" validate:"required_strict"`
|
||||
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
|
||||
Category string `json:"category" validate:"required_strict"`
|
||||
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
|
||||
ProductionStandardId uint `json:"production_standard_id" validate:"required_strict,number,gt=0"`
|
||||
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
|
||||
KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
|
||||
|
||||
@@ -27,9 +27,14 @@ func NewRecordingController(recordingService service.RecordingService) *Recordin
|
||||
func (u *RecordingController) GetAll(c *fiber.Ctx) error {
|
||||
projectFlockID := c.QueryInt("project_flock_kandang_id", 0)
|
||||
|
||||
page := c.QueryInt("page", 1)
|
||||
limit := c.QueryInt("limit", 10)
|
||||
offset := (page - 1) * limit
|
||||
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Search: c.Query("search"),
|
||||
}
|
||||
if projectFlockID > 0 {
|
||||
@@ -79,25 +84,27 @@ func (u *RecordingController) GetOne(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func (u *RecordingController) GetNextDay(c *fiber.Ctx) error {
|
||||
projectFlockID := c.QueryInt("project_flock_kandang_id", 0)
|
||||
if projectFlockID <= 0 {
|
||||
req := new(validation.GetRecordingNextDay)
|
||||
|
||||
if err := c.QueryParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid query params")
|
||||
}
|
||||
if req.ProjectFlockKandangId == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
|
||||
}
|
||||
|
||||
recordTime := time.Now().UTC()
|
||||
if recordDate := strings.TrimSpace(c.Query("record_date")); recordDate != "" {
|
||||
parsed, err := time.Parse("2006-01-02", recordDate)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "record_date must be in YYYY-MM-DD format")
|
||||
}
|
||||
recordTime = parsed.UTC()
|
||||
if req.RecordTime == nil || strings.TrimSpace(*req.RecordTime) == "" {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "record_date is required")
|
||||
}
|
||||
|
||||
nextDay, err := u.RecordingService.GetNextDay(c, uint(projectFlockID), recordTime)
|
||||
recordTime, err := time.Parse("2006-01-02", strings.TrimSpace(*req.RecordTime))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "record_time must be in YYYY-MM-DD format")
|
||||
}
|
||||
req.RecordTimeValue = &recordTime
|
||||
nextDay, err := u.RecordingService.GetNextDay(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
projectFlockID := req.ProjectFlockKandangId
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -73,7 +74,9 @@ type RecordingRelationDTO struct {
|
||||
RecordDatetime time.Time `json:"record_datetime"`
|
||||
Day int `json:"day"`
|
||||
TotalDepletionQty float64 `json:"total_depletion_qty"`
|
||||
TotalDepletionCumQty float64 `json:"total_depletion_cum_qty"`
|
||||
CumDepletionRate float64 `json:"cum_depletion_rate"`
|
||||
DepletionRate float64 `json:"depletion_rate"`
|
||||
CumIntake int `json:"cum_intake"`
|
||||
FcrValue float64 `json:"fcr_value"`
|
||||
HenDay float64 `json:"hen_day"`
|
||||
@@ -230,7 +233,9 @@ func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
|
||||
RecordDatetime: e.RecordDatetime,
|
||||
Day: intValue(e.Day),
|
||||
TotalDepletionQty: floatValue(e.TotalDepletionQty),
|
||||
CumDepletionRate: floatValue(e.CumDepletionRate),
|
||||
TotalDepletionCumQty: floatValue(e.TotalDepletionCumQty),
|
||||
CumDepletionRate: roundFloatValue(e.CumDepletionRate, 2),
|
||||
DepletionRate: roundFloatValue(e.DepletionRate, 2),
|
||||
CumIntake: intValue(e.CumIntake),
|
||||
FcrValue: floatValue(e.FcrValue),
|
||||
HenDay: floatValue(e.HenDay),
|
||||
@@ -275,10 +280,10 @@ func toRecordingProjectFlockDTO(e entity.Recording) RecordingProjectFlockDTO {
|
||||
}
|
||||
}
|
||||
|
||||
if pfk.ProjectFlock.Fcr.Id != 0 || e.StandardFcr != nil {
|
||||
if pfk.ProjectFlock.ProductionStandard.Id != 0 || e.StandardFcr != nil {
|
||||
result.Fcr = &RecordingFcrDTO{
|
||||
Id: pfk.ProjectFlock.Fcr.Id,
|
||||
Name: pfk.ProjectFlock.Fcr.Name,
|
||||
Id: pfk.ProjectFlock.ProductionStandard.Id,
|
||||
Name: pfk.ProjectFlock.ProductionStandard.Name,
|
||||
FcrStd: floatValue(e.StandardFcr),
|
||||
}
|
||||
}
|
||||
@@ -426,6 +431,17 @@ func floatValue(value *float64) float64 {
|
||||
return *value
|
||||
}
|
||||
|
||||
func roundFloatValue(value *float64, places int) float64 {
|
||||
if value == nil {
|
||||
return 0
|
||||
}
|
||||
if places <= 0 {
|
||||
return math.Round(*value)
|
||||
}
|
||||
factor := math.Pow(10, float64(places))
|
||||
return math.Round(*value*factor) / factor
|
||||
}
|
||||
|
||||
func intValue(value *int) int {
|
||||
if value == nil {
|
||||
return 0
|
||||
|
||||
@@ -11,9 +11,17 @@ import (
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
|
||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||
rNonstock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/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"
|
||||
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||
sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services"
|
||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
sProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
||||
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||
@@ -28,9 +36,18 @@ type RecordingModule struct{}
|
||||
|
||||
func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
recordingRepo := rRecording.NewRecordingRepository(db)
|
||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
||||
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
|
||||
projectBudgetRepo := rProjectFlock.NewProjectBudgetRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
flockRepo := rFlock.NewFlockRepository(db)
|
||||
kandangRepo := rKandang.NewKandangRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
nonstockRepo := rNonstock.NewNonstockRepository(db)
|
||||
productRepo := rProduct.NewProductRepository(db)
|
||||
chickinRepo := rChickin.NewChickinRepository(db)
|
||||
chickinDetailRepo := rChickin.NewChickinDetailRepository(db)
|
||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
||||
stockLogRepo := rStockLogs.NewStockLogRepository(db)
|
||||
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
|
||||
@@ -53,14 +70,30 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
TotalQuantity: "total_qty",
|
||||
TotalUsedQuantity: "total_used",
|
||||
CreatedAt: "created_at",
|
||||
CreatedAt: "(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_eggs.recording_id)",
|
||||
},
|
||||
OrderBy: []string{"created_at ASC", "id ASC"},
|
||||
OrderBy: []string{"(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_eggs.recording_id) ASC", "id ASC"},
|
||||
}); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
panic(fmt.Sprintf("failed to register recording egg stockable workflow: %v", err))
|
||||
}
|
||||
}
|
||||
if err := fifoService.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKeyRecordingDepletion,
|
||||
Table: "recording_depletions",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
TotalQuantity: "qty",
|
||||
TotalUsedQuantity: "total_used_qty",
|
||||
CreatedAt: "(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_depletions.recording_id)",
|
||||
},
|
||||
OrderBy: []string{"(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_depletions.recording_id) ASC", "id ASC"},
|
||||
}); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
panic(fmt.Sprintf("failed to register recording depletion stockable workflow: %v", err))
|
||||
}
|
||||
}
|
||||
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
||||
Key: fifo.UsableKeyRecordingStock,
|
||||
Table: "recording_stocks",
|
||||
@@ -69,7 +102,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
UsageQuantity: "usage_qty",
|
||||
PendingQuantity: "pending_qty",
|
||||
CreatedAt: "id",
|
||||
CreatedAt: "(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_stocks.recording_id)",
|
||||
},
|
||||
}); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
@@ -82,9 +115,9 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
Columns: fifo.UsableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "source_product_warehouse_id",
|
||||
UsageQuantity: "qty",
|
||||
UsageQuantity: "usage_qty",
|
||||
PendingQuantity: "pending_qty",
|
||||
CreatedAt: "id",
|
||||
CreatedAt: "(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_depletions.recording_id)",
|
||||
},
|
||||
ExcludedStockables: []fifo.StockableKey{
|
||||
fifo.StockableKeyTransferToLayingIn,
|
||||
@@ -104,9 +137,41 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowRecording, utils.RecordingApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register recording approval workflow: %v", err))
|
||||
}
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlock, utils.ProjectFlockApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
|
||||
}
|
||||
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
projectFlockService := sProjectFlock.NewProjectflockService(
|
||||
projectFlockRepo,
|
||||
flockRepo,
|
||||
kandangRepo,
|
||||
projectFlockKandangRepo,
|
||||
warehouseRepo,
|
||||
productWarehouseRepo,
|
||||
projectBudgetRepo,
|
||||
nonstockRepo,
|
||||
projectFlockPopulationRepo,
|
||||
recordingRepo,
|
||||
approvalService,
|
||||
validate,
|
||||
)
|
||||
|
||||
chickinService := sChickin.NewChickinService(
|
||||
chickinRepo,
|
||||
kandangRepo,
|
||||
warehouseRepo,
|
||||
productWarehouseRepo,
|
||||
productRepo,
|
||||
projectFlockRepo,
|
||||
projectFlockKandangRepo,
|
||||
projectFlockPopulationRepo,
|
||||
chickinDetailRepo,
|
||||
validate,
|
||||
fifoService,
|
||||
)
|
||||
|
||||
recordingService := sRecording.NewRecordingService(
|
||||
recordingRepo,
|
||||
projectFlockKandangRepo,
|
||||
@@ -117,6 +182,8 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
fifoService,
|
||||
stockLogRepo,
|
||||
productionStandardService,
|
||||
projectFlockService,
|
||||
chickinService,
|
||||
validate,
|
||||
)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
@@ -17,8 +17,13 @@ type RecordingRepository interface {
|
||||
repository.BaseRepository[entity.Recording]
|
||||
|
||||
WithRelations(db *gorm.DB) *gorm.DB
|
||||
WithRelationsList(db *gorm.DB) *gorm.DB
|
||||
ApplyListFilters(db *gorm.DB, search string, projectFlockKandangId uint) *gorm.DB
|
||||
ApplyListCountFilters(db *gorm.DB, search string, projectFlockKandangId uint) *gorm.DB
|
||||
ApplySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB
|
||||
GetAllWithFilters(ctx context.Context, offset, limit int, search string, projectFlockKandangId uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Recording, int64, error)
|
||||
GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error)
|
||||
ListByProjectFlockKandangID(ctx context.Context, tx *gorm.DB, projectFlockKandangId uint, from *time.Time) ([]entity.Recording, error)
|
||||
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
|
||||
|
||||
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error
|
||||
@@ -39,13 +44,18 @@ type RecordingRepository interface {
|
||||
ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error)
|
||||
|
||||
SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error)
|
||||
GetCumulativeDepletionByProjectFlockKandangUntil(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error)
|
||||
GetCumulativeDepletionByRecordingIDs(tx *gorm.DB, recordingIDs []uint) (map[uint]float64, error)
|
||||
GetUniformityMeanBwByWeek(tx *gorm.DB, projectFlockKandangId uint, week int) (float64, bool, error)
|
||||
FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error)
|
||||
GetPreviousTotalChickByRecordingIDs(tx *gorm.DB, recordingIDs []uint) (map[uint]*float64, error)
|
||||
GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error)
|
||||
GetRemainingPopulationByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint) (float64, error)
|
||||
GetTotalChickByProjectFlockKandangIDs(tx *gorm.DB, projectFlockKandangIds []uint) (map[uint]int64, error)
|
||||
GetTotalChickinByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint) (float64, error)
|
||||
GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error)
|
||||
GetEggSummaryByRecording(tx *gorm.DB, recordingID uint) (totalQty float64, totalWeightGrams float64, err error)
|
||||
GetCumulativeEggQtyByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error)
|
||||
GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error)
|
||||
GetTotalWeightProducedFromUniformityByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error)
|
||||
GetTotalWeightProducedFromUniformityByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||
GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error)
|
||||
@@ -54,6 +64,8 @@ type RecordingRepository interface {
|
||||
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
|
||||
GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error)
|
||||
GetAverageTargetMetricsByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, includeTargets bool) (RecordingTargetAverages, error)
|
||||
ResyncProjectFlockPopulationUsage(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error
|
||||
ValidateProductWarehousesByFlags(ctx context.Context, ids []uint, flags []string) (uint, error)
|
||||
}
|
||||
|
||||
type RecordingRepositoryImpl struct {
|
||||
@@ -91,7 +103,7 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
||||
Preload("ProjectFlockKandang.Kandang.Location").
|
||||
Preload("ProjectFlockKandang.ProjectFlock").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.Fcr").
|
||||
// Preload("ProjectFlockKandang.ProjectFlock.Fcr").
|
||||
Preload("Depletions").
|
||||
Preload("Depletions.ProductWarehouse").
|
||||
Preload("Depletions.ProductWarehouse.Product").
|
||||
@@ -112,6 +124,64 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
||||
Preload("Eggs.ProductWarehouse.Warehouse.Location")
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) WithRelationsList(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("CreatedUser").
|
||||
Preload("ProjectFlockKandang").
|
||||
Preload("ProjectFlockKandang.Kandang").
|
||||
Preload("ProjectFlockKandang.Kandang.Location").
|
||||
Preload("ProjectFlockKandang.ProjectFlock").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard")
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) ApplyListFilters(db *gorm.DB, search string, projectFlockKandangId uint) *gorm.DB {
|
||||
db = r.WithRelationsList(db)
|
||||
db = db.
|
||||
Joins("JOIN project_flock_kandangs pfk ON pfk.id = recordings.project_flock_kandangs_id").
|
||||
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id")
|
||||
if projectFlockKandangId != 0 {
|
||||
db = db.Where("recordings.project_flock_kandangs_id = ?", projectFlockKandangId)
|
||||
}
|
||||
db = r.ApplySearchFilters(db, search)
|
||||
return db.Order("recordings.record_datetime DESC").Order("recordings.created_at DESC")
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) ApplyListCountFilters(db *gorm.DB, search string, projectFlockKandangId uint) *gorm.DB {
|
||||
db = db.
|
||||
Joins("JOIN project_flock_kandangs pfk ON pfk.id = recordings.project_flock_kandangs_id").
|
||||
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id")
|
||||
if projectFlockKandangId != 0 {
|
||||
db = db.Where("recordings.project_flock_kandangs_id = ?", projectFlockKandangId)
|
||||
}
|
||||
db = r.ApplySearchFilters(db, search)
|
||||
return db
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, search string, projectFlockKandangId uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Recording, int64, error) {
|
||||
var (
|
||||
records []entity.Recording
|
||||
total int64
|
||||
)
|
||||
|
||||
countQ := r.ApplyListCountFilters(r.DB().WithContext(ctx).Model(&entity.Recording{}), search, projectFlockKandangId)
|
||||
if modifier != nil {
|
||||
countQ = modifier(countQ)
|
||||
}
|
||||
if err := countQ.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
listQ := r.ApplyListFilters(r.DB().WithContext(ctx).Model(&entity.Recording{}), search, projectFlockKandangId)
|
||||
if modifier != nil {
|
||||
listQ = modifier(listQ)
|
||||
}
|
||||
if err := listQ.Offset(offset).Limit(limit).Find(&records).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return records, total, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) ApplySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB {
|
||||
normalized := strings.ToLower(strings.TrimSpace(rawSearch))
|
||||
if normalized == "" {
|
||||
@@ -169,6 +239,27 @@ func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.C
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) ListByProjectFlockKandangID(ctx context.Context, tx *gorm.DB, projectFlockKandangId uint, from *time.Time) ([]entity.Recording, error) {
|
||||
if projectFlockKandangId == 0 {
|
||||
return nil, errors.New("project_flock_kandang_id is required")
|
||||
}
|
||||
|
||||
db := tx.WithContext(ctx).
|
||||
Model(&entity.Recording{}).
|
||||
Where("project_flock_kandangs_id = ?", projectFlockKandangId).
|
||||
Where("deleted_at IS NULL")
|
||||
|
||||
if from != nil {
|
||||
db = db.Where("record_datetime >= ?", *from)
|
||||
}
|
||||
|
||||
var records []entity.Recording
|
||||
if err := db.Order("record_datetime ASC").Order("created_at ASC").Find(&records).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
|
||||
var days []int
|
||||
if err := tx.Model(&entity.Recording{}).
|
||||
@@ -314,6 +405,85 @@ func (r *RecordingRepositoryImpl) SumRecordingDepletions(tx *gorm.DB, recordingI
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetCumulativeDepletionByProjectFlockKandangUntil(tx *gorm.DB, projectFlockKandangId uint, recordTime time.Time) (float64, error) {
|
||||
if projectFlockKandangId == 0 || recordTime.IsZero() {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var total float64
|
||||
err := tx.
|
||||
Table("recording_depletions rd").
|
||||
Select("COALESCE(SUM(rd.qty),0)").
|
||||
Joins("JOIN recordings r ON r.id = rd.recording_id").
|
||||
Where("r.project_flock_kandangs_id = ?", projectFlockKandangId).
|
||||
Where("r.record_datetime <= ?", recordTime).
|
||||
Where("r.deleted_at IS NULL").
|
||||
Scan(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetCumulativeDepletionByRecordingIDs(tx *gorm.DB, recordingIDs []uint) (map[uint]float64, error) {
|
||||
result := make(map[uint]float64)
|
||||
if len(recordingIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type row struct {
|
||||
RecordingID uint `gorm:"column:recording_id"`
|
||||
Total float64 `gorm:"column:total_qty"`
|
||||
}
|
||||
var rows []row
|
||||
|
||||
err := tx.
|
||||
Table("recordings r").
|
||||
Select("r.id AS recording_id, COALESCE(SUM(rd.qty), 0) AS total_qty").
|
||||
Joins(`
|
||||
LEFT JOIN recordings r2
|
||||
ON r2.project_flock_kandangs_id = r.project_flock_kandangs_id
|
||||
AND r2.record_datetime <= r.record_datetime
|
||||
AND r2.deleted_at IS NULL`).
|
||||
Joins("LEFT JOIN recording_depletions rd ON rd.recording_id = r2.id").
|
||||
Where("r.id IN ?", recordingIDs).
|
||||
Where("r.deleted_at IS NULL").
|
||||
Group("r.id").
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
result[row.RecordingID] = row.Total
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetUniformityMeanBwByWeek(tx *gorm.DB, projectFlockKandangId uint, week int) (float64, bool, error) {
|
||||
if projectFlockKandangId == 0 || week <= 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
var row struct {
|
||||
ID uint
|
||||
MeanUp float64
|
||||
}
|
||||
if err := tx.
|
||||
Table("project_flock_kandang_uniformity").
|
||||
Select("id, mean_up").
|
||||
Where("project_flock_kandang_id = ?", projectFlockKandangId).
|
||||
Where("week = ?", week).
|
||||
Order("id DESC").
|
||||
Limit(1).
|
||||
Scan(&row).Error; err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
if row.ID == 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
meanBw := row.MeanUp / 1.10
|
||||
return meanBw, true, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) {
|
||||
if currentDay <= 1 {
|
||||
return nil, nil
|
||||
@@ -336,6 +506,46 @@ func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFloc
|
||||
return &prev, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetPreviousTotalChickByRecordingIDs(tx *gorm.DB, recordingIDs []uint) (map[uint]*float64, error) {
|
||||
result := make(map[uint]*float64)
|
||||
if len(recordingIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type row struct {
|
||||
RecordingID uint `gorm:"column:recording_id"`
|
||||
PrevTotalChickQty *float64 `gorm:"column:prev_total_chick_qty"`
|
||||
}
|
||||
var rows []row
|
||||
|
||||
err := tx.
|
||||
Table("recordings r").
|
||||
Select(`
|
||||
r.id AS recording_id,
|
||||
(
|
||||
SELECT r2.total_chick_qty
|
||||
FROM recordings r2
|
||||
WHERE r2.project_flock_kandangs_id = r.project_flock_kandangs_id
|
||||
AND r2.day IS NOT NULL
|
||||
AND r.day IS NOT NULL
|
||||
AND r2.day < r.day
|
||||
AND r2.deleted_at IS NULL
|
||||
ORDER BY r2.day DESC
|
||||
LIMIT 1
|
||||
) AS prev_total_chick_qty`).
|
||||
Where("r.id IN ?", recordingIDs).
|
||||
Where("r.deleted_at IS NULL").
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
result[row.RecordingID] = row.PrevTotalChickQty
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) {
|
||||
var total float64
|
||||
err := tx.
|
||||
@@ -355,6 +565,57 @@ func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandang
|
||||
return int64(math.Round(total)), nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetRemainingPopulationByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint) (float64, error) {
|
||||
var total float64
|
||||
err := tx.
|
||||
Table("project_flock_populations").
|
||||
Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0) AS total_qty").
|
||||
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
||||
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangId).
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if total < 0 {
|
||||
total = 0
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetTotalChickByProjectFlockKandangIDs(tx *gorm.DB, projectFlockKandangIds []uint) (map[uint]int64, error) {
|
||||
result := make(map[uint]int64)
|
||||
if len(projectFlockKandangIds) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type row struct {
|
||||
ProjectFlockKandangId uint `gorm:"column:project_flock_kandang_id"`
|
||||
Total float64 `gorm:"column:total_qty"`
|
||||
}
|
||||
var rows []row
|
||||
|
||||
err := tx.
|
||||
Table("project_flock_populations pfp").
|
||||
Select("project_chickins.project_flock_kandang_id, COALESCE(SUM(pfp.total_qty - pfp.total_used_qty), 0) AS total_qty").
|
||||
Joins("JOIN project_chickins ON project_chickins.id = pfp.project_chickin_id").
|
||||
Where("project_chickins.project_flock_kandang_id IN ?", projectFlockKandangIds).
|
||||
Group("project_chickins.project_flock_kandang_id").
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
total := math.Round(row.Total)
|
||||
if total < 0 {
|
||||
total = 0
|
||||
}
|
||||
result[row.ProjectFlockKandangId] = int64(total)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm.DB, projectFlockKandangId uint) (float64, error) {
|
||||
if projectFlockKandangId == 0 {
|
||||
return 0, nil
|
||||
@@ -430,34 +691,6 @@ func (r *RecordingRepositoryImpl) GetCumulativeEggQtyByProjectFlockKandang(
|
||||
Scan(&result).Error
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetFcrStandardNumber(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) {
|
||||
if fcrId == 0 || currentWeightKg <= 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
var standard entity.FcrStandard
|
||||
err := tx.
|
||||
Where("fcr_id = ? AND weight >= ?", fcrId, currentWeightKg).
|
||||
Order("weight ASC").
|
||||
First(&standard).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = tx.
|
||||
Where("fcr_id = ?", fcrId).
|
||||
Order("weight DESC").
|
||||
First(&standard).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, false, nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
return standard.FcrNumber, true, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) {
|
||||
// Body-weight tracking is removed; keep stub for report compatibility.
|
||||
return 0, 0, nil
|
||||
@@ -568,6 +801,82 @@ func (r *RecordingRepositoryImpl) GetAverageTargetMetricsByProjectFlockKandangID
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) ResyncProjectFlockPopulationUsage(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
|
||||
if projectFlockKandangID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
idsSubquery := `
|
||||
SELECT pfp.id
|
||||
FROM project_flock_populations pfp
|
||||
JOIN project_chickins pc ON pc.id = pfp.project_chickin_id
|
||||
WHERE pc.project_flock_kandang_id = ?
|
||||
`
|
||||
|
||||
updateWithAlloc := `
|
||||
UPDATE project_flock_populations p
|
||||
SET total_used_qty = COALESCE(a.used, 0)
|
||||
FROM (
|
||||
SELECT stockable_id, SUM(qty) AS used
|
||||
FROM stock_allocations
|
||||
WHERE stockable_type = 'PROJECT_FLOCK_POPULATION'
|
||||
AND status = 'ACTIVE'
|
||||
GROUP BY stockable_id
|
||||
) a
|
||||
WHERE p.id = a.stockable_id
|
||||
AND p.id IN (` + idsSubquery + `)
|
||||
`
|
||||
|
||||
resetMissing := `
|
||||
UPDATE project_flock_populations p
|
||||
SET total_used_qty = 0
|
||||
WHERE p.id IN (` + idsSubquery + `)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM stock_allocations sa
|
||||
WHERE sa.stockable_type = 'PROJECT_FLOCK_POPULATION'
|
||||
AND sa.status = 'ACTIVE'
|
||||
AND sa.stockable_id = p.id
|
||||
)
|
||||
`
|
||||
|
||||
db := r.DB().WithContext(ctx)
|
||||
if tx != nil {
|
||||
db = tx.WithContext(ctx)
|
||||
}
|
||||
|
||||
if err := db.Exec(updateWithAlloc, projectFlockKandangID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Exec(resetMissing, projectFlockKandangID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) ValidateProductWarehousesByFlags(ctx context.Context, ids []uint, flags []string) (uint, error) {
|
||||
if len(ids) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
var invalidIDs []uint
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Table("product_warehouses pw").
|
||||
Where("pw.id IN ?", ids).
|
||||
Where(`NOT EXISTS (
|
||||
SELECT 1 FROM flags f
|
||||
WHERE f.flagable_type = 'products'
|
||||
AND f.flagable_id = pw.product_id
|
||||
AND UPPER(f.name) IN ?
|
||||
)`, flags).
|
||||
Pluck("pw.id", &invalidIDs).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(invalidIDs) > 0 {
|
||||
return invalidIDs[0], nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func nextRecordingDay(days []int) int {
|
||||
if len(days) == 0 {
|
||||
return 1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
package validation
|
||||
|
||||
import "time"
|
||||
|
||||
type (
|
||||
Stock struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||
@@ -35,6 +37,7 @@ type Update struct {
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
Offset int `query:"-" validate:"omitempty,number,min=0"`
|
||||
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
}
|
||||
@@ -44,3 +47,9 @@ type Approve struct {
|
||||
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
||||
type GetRecordingNextDay struct {
|
||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" query:"project_flock_kandang_id" validate:"required,number,min=1"`
|
||||
RecordTime *string `json:"record_date" query:"record_date" validate:"required,datetime=2006-01-02"`
|
||||
RecordTimeValue *time.Time `query:"-" validate:"-"`
|
||||
}
|
||||
|
||||
@@ -465,11 +465,16 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.EqualFold(category, string(utils.ProjectFlockCategoryGrowing)) {
|
||||
if err := s.updateGrowingFcrForWeek(tx, createBody.ProjectFlockKandangId, createBody.Week, calculation.MeanUp); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
s.Log.Errorf("Failed to create uniformity: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if s.DocumentSvc != nil {
|
||||
actorIDCopy := actorID
|
||||
@@ -633,6 +638,9 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.updateGrowingFcrFromUniformity(c.Context(), id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
@@ -694,6 +702,10 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.updateGrowingFcrFromUniformity(c.Context(), id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
@@ -724,7 +736,48 @@ func (s uniformityService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||
type uniformityContext struct {
|
||||
ID uint
|
||||
Week int
|
||||
ProjectFlockKandangId uint
|
||||
Category string
|
||||
}
|
||||
var ctxRow uniformityContext
|
||||
if err := s.Repository.DB().WithContext(c.Context()).
|
||||
Table("project_flock_kandang_uniformity u").
|
||||
Select("u.id, u.week, u.project_flock_kandang_id, pf.category").
|
||||
Joins("JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id").
|
||||
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
|
||||
Where("u.id = ?", id).
|
||||
Scan(&ctxRow).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctxRow.ID == 0 {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
|
||||
}
|
||||
|
||||
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
repoTx := s.Repository.WithTx(tx)
|
||||
if err := repoTx.DeleteOne(c.Context(), id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.EqualFold(ctxRow.Category, string(utils.ProjectFlockCategoryGrowing)) {
|
||||
startDay := (ctxRow.Week-1)*7 + 1
|
||||
endDay := ctxRow.Week * 7
|
||||
if ctxRow.Week > 0 {
|
||||
if err := tx.Model(&entity.Recording{}).
|
||||
Where("project_flock_kandangs_id = ?", ctxRow.ProjectFlockKandangId).
|
||||
Where("day BETWEEN ? AND ?", startDay, endDay).
|
||||
Update("fcr_value", 0).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
|
||||
}
|
||||
@@ -734,6 +787,58 @@ func (s uniformityService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *uniformityService) updateGrowingFcrFromUniformity(ctx context.Context, uniformityID uint) error {
|
||||
if uniformityID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type uniformityRow struct {
|
||||
ID uint
|
||||
Week int
|
||||
MeanUp float64
|
||||
ProjectFlockKandangId uint
|
||||
Category string
|
||||
}
|
||||
var row uniformityRow
|
||||
if err := s.Repository.DB().WithContext(ctx).
|
||||
Table("project_flock_kandang_uniformity u").
|
||||
Select("u.id, u.week, u.mean_up, u.project_flock_kandang_id, pf.category").
|
||||
Joins("JOIN project_flock_kandangs pfk ON pfk.id = u.project_flock_kandang_id").
|
||||
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
|
||||
Where("u.id = ?", uniformityID).
|
||||
Scan(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if row.ID == 0 {
|
||||
return nil
|
||||
}
|
||||
if !strings.EqualFold(row.Category, string(utils.ProjectFlockCategoryGrowing)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.updateGrowingFcrForWeek(s.Repository.DB().WithContext(ctx), row.ProjectFlockKandangId, row.Week, row.MeanUp)
|
||||
}
|
||||
|
||||
func (s *uniformityService) updateGrowingFcrForWeek(tx *gorm.DB, projectFlockKandangID uint, week int, meanUp float64) error {
|
||||
if tx == nil || projectFlockKandangID == 0 || week <= 0 {
|
||||
return nil
|
||||
}
|
||||
startDay := (week-1)*7 + 1
|
||||
endDay := week * 7
|
||||
meanBw := meanUp / 1.10
|
||||
if meanBw <= 0 {
|
||||
return tx.Model(&entity.Recording{}).
|
||||
Where("project_flock_kandangs_id = ?", projectFlockKandangID).
|
||||
Where("day BETWEEN ? AND ?", startDay, endDay).
|
||||
Update("fcr_value", 0).Error
|
||||
}
|
||||
|
||||
return tx.Model(&entity.Recording{}).
|
||||
Where("project_flock_kandangs_id = ?", projectFlockKandangID).
|
||||
Where("day BETWEEN ? AND ?", startDay, endDay).
|
||||
Update("fcr_value", gorm.Expr("CASE WHEN feed_intake IS NULL OR feed_intake = 0 THEN 0 ELSE feed_intake / ? END", meanBw)).Error
|
||||
}
|
||||
|
||||
func (s uniformityService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -27,6 +28,11 @@ type PurchaseListDTO struct {
|
||||
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
|
||||
DueDate *time.Time `json:"due_date"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||
RequesterName string `json:"requester_name"`
|
||||
PoExpedition []PoExpeditionDTO `json:"po_expedition"`
|
||||
Products []productDTO.ProductRelationDTO `json:"products"`
|
||||
Location *locationDTO.LocationRelationDTO `json:"location"`
|
||||
Area *areaDTO.AreaRelationDTO `json:"area"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
||||
@@ -61,6 +67,12 @@ type PurchaseItemDTO struct {
|
||||
VehicleNumber *string `json:"vehicle_number"`
|
||||
TransportPerItem *float64 `json:"transport_per_item,omitempty"`
|
||||
ExpeditionVendor *supplierDTO.SupplierRelationDTO `json:"expedition_vendor,omitempty"`
|
||||
HasChickin bool `json:"has_chickin"`
|
||||
}
|
||||
|
||||
type PoExpeditionDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
Refrence string `json:"refrence"`
|
||||
}
|
||||
|
||||
func ToPurchaseRelationDTO(p *entity.Purchase) PurchaseRelationDTO {
|
||||
@@ -89,6 +101,7 @@ func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
|
||||
TravelNumber: item.TravelNumber,
|
||||
TravelDocumentPath: item.TravelNumberDocs,
|
||||
VehicleNumber: item.VehicleNumber,
|
||||
HasChickin: item.HasChickin,
|
||||
}
|
||||
|
||||
if item.Product != nil && item.Product.Id != 0 {
|
||||
@@ -146,6 +159,10 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO {
|
||||
mapped := userDTO.ToUserRelationDTO(p.CreatedUser)
|
||||
createdUser = &mapped
|
||||
}
|
||||
requesterName := ""
|
||||
if createdUser != nil {
|
||||
requesterName = createdUser.Name
|
||||
}
|
||||
|
||||
var latestApproval *approvalDTO.ApprovalRelationDTO
|
||||
if p.LatestApproval != nil && p.LatestApproval.Id != 0 {
|
||||
@@ -153,11 +170,57 @@ func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO {
|
||||
latestApproval = &mapped
|
||||
}
|
||||
|
||||
var (
|
||||
poExpedition = make([]PoExpeditionDTO, 0)
|
||||
location *locationDTO.LocationRelationDTO
|
||||
area *areaDTO.AreaRelationDTO
|
||||
)
|
||||
productMap := make(map[uint]productDTO.ProductRelationDTO)
|
||||
expeditionRefSet := make(map[uint64]struct{})
|
||||
for i := range p.Items {
|
||||
item := p.Items[i]
|
||||
if item.Product != nil && item.Product.Id != 0 {
|
||||
if _, exists := productMap[item.Product.Id]; !exists {
|
||||
productMap[item.Product.Id] = productDTO.ToProductRelationDTO(*item.Product)
|
||||
}
|
||||
}
|
||||
if item.ExpenseNonstock != nil && item.ExpenseNonstock.Expense != nil {
|
||||
exp := item.ExpenseNonstock.Expense
|
||||
ref := strings.TrimSpace(exp.ReferenceNumber)
|
||||
if exp.Id != 0 && ref != "" {
|
||||
if _, exists := expeditionRefSet[exp.Id]; !exists {
|
||||
expeditionRefSet[exp.Id] = struct{}{}
|
||||
poExpedition = append(poExpedition, PoExpeditionDTO{
|
||||
Id: exp.Id,
|
||||
Refrence: ref,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if location == nil && item.Warehouse != nil && item.Warehouse.Location != nil && item.Warehouse.Location.Id != 0 {
|
||||
loc := locationDTO.ToLocationRelationDTO(*item.Warehouse.Location)
|
||||
location = &loc
|
||||
}
|
||||
if area == nil && item.Warehouse != nil && item.Warehouse.Area.Id != 0 {
|
||||
ar := areaDTO.ToAreaRelationDTO(item.Warehouse.Area)
|
||||
area = &ar
|
||||
}
|
||||
}
|
||||
products := make([]productDTO.ProductRelationDTO, 0, len(productMap))
|
||||
for _, prod := range productMap {
|
||||
products = append(products, prod)
|
||||
}
|
||||
|
||||
return PurchaseListDTO{
|
||||
PurchaseRelationDTO: ToPurchaseRelationDTO(&p),
|
||||
Supplier: supplier,
|
||||
DueDate: p.DueDate,
|
||||
CreatedUser: createdUser,
|
||||
RequesterName: requesterName,
|
||||
PoExpedition: poExpedition,
|
||||
Products: products,
|
||||
Location: location,
|
||||
Area: area,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
LatestApproval: latestApproval,
|
||||
|
||||
@@ -255,6 +255,39 @@ func (s *purchaseService) GetOne(c *fiber.Ctx, id uint) (*entity.Purchase, error
|
||||
if err := s.attachLatestApproval(c.Context(), purchase); err != nil {
|
||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err)
|
||||
}
|
||||
if len(purchase.Items) > 0 {
|
||||
itemIDs := make([]uint, 0, len(purchase.Items))
|
||||
for i := range purchase.Items {
|
||||
if purchase.Items[i].Id == 0 {
|
||||
continue
|
||||
}
|
||||
itemIDs = append(itemIDs, purchase.Items[i].Id)
|
||||
}
|
||||
if len(itemIDs) > 0 {
|
||||
var usedIDs []uint
|
||||
if err := s.PurchaseRepo.DB().WithContext(c.Context()).
|
||||
Model(&entity.StockAllocation{}).
|
||||
Distinct("stockable_id").
|
||||
Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?",
|
||||
fifo.StockableKeyPurchaseItems.String(),
|
||||
itemIDs,
|
||||
fifo.UsableKeyProjectChickin.String(),
|
||||
[]string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending},
|
||||
).
|
||||
Pluck("stockable_id", &usedIDs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usedSet := make(map[uint]struct{}, len(usedIDs))
|
||||
for _, id := range usedIDs {
|
||||
usedSet[id] = struct{}{}
|
||||
}
|
||||
for i := range purchase.Items {
|
||||
if _, ok := usedSet[purchase.Items[i].Id]; ok {
|
||||
purchase.Items[i].HasChickin = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.applyTravelDocumentURLs(c.Context(), purchase)
|
||||
|
||||
return purchase, nil
|
||||
@@ -498,6 +531,54 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
||||
return nil, utils.BadRequest("Items must not be empty for staff approval")
|
||||
}
|
||||
|
||||
if action == entity.ApprovalActionApproved {
|
||||
itemIDs := make([]uint, 0, len(purchase.Items))
|
||||
itemByID := make(map[uint]entity.PurchaseItem, len(purchase.Items))
|
||||
for i := range purchase.Items {
|
||||
if purchase.Items[i].Id == 0 {
|
||||
continue
|
||||
}
|
||||
itemIDs = append(itemIDs, purchase.Items[i].Id)
|
||||
itemByID[purchase.Items[i].Id] = purchase.Items[i]
|
||||
}
|
||||
if len(itemIDs) > 0 {
|
||||
var usedIDs []uint
|
||||
if err := s.PurchaseRepo.DB().WithContext(ctx).
|
||||
Model(&entity.StockAllocation{}).
|
||||
Distinct("stockable_id").
|
||||
Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?",
|
||||
fifo.StockableKeyPurchaseItems.String(),
|
||||
itemIDs,
|
||||
fifo.UsableKeyProjectChickin.String(),
|
||||
[]string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending},
|
||||
).
|
||||
Pluck("stockable_id", &usedIDs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(usedIDs) > 0 {
|
||||
usedSet := make(map[uint]struct{}, len(usedIDs))
|
||||
for _, id := range usedIDs {
|
||||
usedSet[id] = struct{}{}
|
||||
}
|
||||
for _, payload := range req.Items {
|
||||
if payload.PurchaseItemID == 0 || payload.Qty == nil {
|
||||
continue
|
||||
}
|
||||
if _, used := usedSet[payload.PurchaseItemID]; !used {
|
||||
continue
|
||||
}
|
||||
item, ok := itemByID[payload.PurchaseItemID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if *payload.Qty != item.SubQty {
|
||||
return nil, utils.BadRequest("Purchase sudah chickin, qty tidak bisa diubah")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
payload, err := s.buildStaffAdjustmentPayload(c.Context(), purchase, req, syncReceiving)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -745,6 +826,54 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
req.Items[idx].TravelDocumentPath = &uploadedURL
|
||||
}
|
||||
}
|
||||
if action == entity.ApprovalActionApproved {
|
||||
itemIDs := make([]uint, 0, len(purchase.Items))
|
||||
itemByID := make(map[uint]entity.PurchaseItem, len(purchase.Items))
|
||||
for i := range purchase.Items {
|
||||
if purchase.Items[i].Id == 0 {
|
||||
continue
|
||||
}
|
||||
itemIDs = append(itemIDs, purchase.Items[i].Id)
|
||||
itemByID[purchase.Items[i].Id] = purchase.Items[i]
|
||||
}
|
||||
if len(itemIDs) > 0 {
|
||||
var usedIDs []uint
|
||||
if err := s.PurchaseRepo.DB().WithContext(ctx).
|
||||
Model(&entity.StockAllocation{}).
|
||||
Distinct("stockable_id").
|
||||
Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?",
|
||||
fifo.StockableKeyPurchaseItems.String(),
|
||||
itemIDs,
|
||||
fifo.UsableKeyProjectChickin.String(),
|
||||
[]string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending},
|
||||
).
|
||||
Pluck("stockable_id", &usedIDs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(usedIDs) > 0 {
|
||||
usedSet := make(map[uint]struct{}, len(usedIDs))
|
||||
for _, id := range usedIDs {
|
||||
usedSet[id] = struct{}{}
|
||||
}
|
||||
for _, payload := range req.Items {
|
||||
if _, used := usedSet[payload.PurchaseItemID]; !used {
|
||||
continue
|
||||
}
|
||||
item, ok := itemByID[payload.PurchaseItemID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
receivedQty := item.SubQty
|
||||
if payload.ReceivedQty != nil {
|
||||
receivedQty = *payload.ReceivedQty
|
||||
}
|
||||
if receivedQty != item.TotalQty {
|
||||
return nil, utils.BadRequest("Purchase sudah chickin, qty penerimaan tidak bisa diubah")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemMap := make(map[uint]*entity.PurchaseItem, len(purchase.Items))
|
||||
for i := range purchase.Items {
|
||||
@@ -912,6 +1041,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
pwID uint
|
||||
qty float64
|
||||
}, 0, len(prepared))
|
||||
resolvePendingIDs := make(map[uint]struct{})
|
||||
logEntries := make([]struct {
|
||||
itemID uint
|
||||
pwID uint
|
||||
@@ -934,6 +1064,15 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Safety: ensure the PW we got matches the purchase item product.
|
||||
if pwDetail, err := pwRepoTx.GetDetailByID(c.Context(), pwID); err != nil {
|
||||
return err
|
||||
} else if pwDetail.ProductId != uint(item.ProductId) {
|
||||
return fiber.NewError(
|
||||
fiber.StatusBadRequest,
|
||||
fmt.Sprintf("Product warehouse %d belongs to product %d, not purchase item product %d", pwID, pwDetail.ProductId, item.ProductId),
|
||||
)
|
||||
}
|
||||
newPWID = &pwID
|
||||
|
||||
deltaQty := prep.receivedQty - item.TotalQty
|
||||
@@ -952,6 +1091,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
pwID uint
|
||||
qty float64
|
||||
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
|
||||
resolvePendingIDs[*newPWID] = struct{}{}
|
||||
} else {
|
||||
deltas[*newPWID] += deltaQty
|
||||
totalQtyDeltas[item.Id] += deltaQty
|
||||
@@ -964,11 +1104,14 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
qty float64
|
||||
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
|
||||
affected[*newPWID] = struct{}{}
|
||||
resolvePendingIDs[*newPWID] = struct{}{}
|
||||
} else {
|
||||
deltas[*newPWID] += deltaQty // negative
|
||||
affected[*newPWID] = struct{}{}
|
||||
totalQtyDeltas[item.Id] += deltaQty
|
||||
}
|
||||
case newPWID != nil:
|
||||
resolvePendingIDs[*newPWID] = struct{}{}
|
||||
}
|
||||
|
||||
dateCopy := prep.receivedDate
|
||||
@@ -1066,6 +1209,19 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
return err
|
||||
}
|
||||
}
|
||||
for pwID := range resolvePendingIDs {
|
||||
if pwID == 0 {
|
||||
continue
|
||||
}
|
||||
resolved, err := s.FifoSvc.ResolvePending(c.Context(), commonSvc.PendingResolveRequest{
|
||||
ProductWarehouseID: pwID,
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Log.Infof("ResolvePending purchase=%d pw=%d resolved=%d", purchase.Id, pwID, len(resolved))
|
||||
}
|
||||
}
|
||||
|
||||
if len(logEntries) > 0 {
|
||||
@@ -1349,6 +1505,30 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
||||
}
|
||||
|
||||
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
itemIDs := make([]uint, 0, len(itemsToDelete))
|
||||
for _, item := range itemsToDelete {
|
||||
if item.Id == 0 {
|
||||
continue
|
||||
}
|
||||
itemIDs = append(itemIDs, item.Id)
|
||||
}
|
||||
if len(itemIDs) > 0 {
|
||||
var count int64
|
||||
if err := tx.Model(&entity.StockAllocation{}).
|
||||
Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?",
|
||||
fifo.StockableKeyPurchaseItems.String(),
|
||||
itemIDs,
|
||||
fifo.UsableKeyProjectChickin.String(),
|
||||
[]string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending},
|
||||
).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return utils.BadRequest("Purchase already chickin, failed to delete purchase")
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.rollbackPurchaseStock(ctx, tx, itemsToDelete, note, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1365,6 +1545,10 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
||||
return nil
|
||||
})
|
||||
if transactionErr != nil {
|
||||
var fe *fiber.Error
|
||||
if errors.As(transactionErr, &fe) {
|
||||
return fe
|
||||
}
|
||||
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
||||
return utils.NotFound("Purchase not found")
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ type ReceivePurchaseItemRequest struct {
|
||||
|
||||
type ReceivePurchaseRequest struct {
|
||||
Action string `form:"action" json:"action" validate:"required,oneof=APPROVED REJECTED"`
|
||||
Items []ReceivePurchaseItemRequest `form:"items" json:"items" validate:"min=1,dive"`
|
||||
Items []ReceivePurchaseItemRequest `form:"items" json:"items" validate:"omitempty,dive"`
|
||||
TravelDocuments []*multipart.FileHeader `form:"travel_documents" json:"-" validate:"omitempty,dive"`
|
||||
Notes *string `form:"notes" json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
||||
@@ -324,6 +324,7 @@ func (c *RepportController) GetCustomerPayment(ctx *fiber.Ctx) error {
|
||||
Page: ctx.QueryInt("page", 1),
|
||||
Limit: ctx.QueryInt("limit", 10),
|
||||
CustomerIDs: customerIDs,
|
||||
FilterBy: strings.ToUpper(ctx.Query("filter_by", "")),
|
||||
StartDate: ctx.Query("start_date", ""),
|
||||
EndDate: ctx.Query("end_date", ""),
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -208,6 +209,75 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
}
|
||||
}
|
||||
|
||||
feedRows := make([]struct {
|
||||
ProjectFlockKandangID uint
|
||||
FeedCost float64
|
||||
SupplierID *uint
|
||||
SupplierName *string
|
||||
SupplierAlias *string
|
||||
}, 0)
|
||||
|
||||
feedQuery := r.db.WithContext(ctx).
|
||||
Table("recordings AS r").
|
||||
Select(`
|
||||
r.project_flock_kandangs_id AS project_flock_kandang_id,
|
||||
s.id AS supplier_id,
|
||||
s.name AS supplier_name,
|
||||
s.alias AS supplier_alias`).
|
||||
Joins("JOIN recording_stocks AS rs ON rs.recording_id = r.id").
|
||||
Joins("JOIN product_warehouses AS pw ON pw.id = rs.product_warehouse_id").
|
||||
Joins("JOIN flags AS f ON f.flagable_id = pw.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
|
||||
Joins("JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.stockable_type = ?", fifo.UsableKeyRecordingStock.String(), fifo.StockableKeyPurchaseItems.String()).
|
||||
Joins("JOIN purchase_items AS pi ON pi.id = sa.stockable_id").
|
||||
Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id").
|
||||
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
|
||||
Where("r.project_flock_kandangs_id IN ?", projectFlockKandangIDs).
|
||||
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
|
||||
Where("f.name = ?", utils.FlagPakan).
|
||||
Group("r.project_flock_kandangs_id, s.id, s.name, s.alias")
|
||||
|
||||
if err := feedQuery.Scan(&feedRows).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
feedSuppliers := make([]HppPerKandangSupplierRow, 0)
|
||||
feedSeen := make(map[uint]map[uint]bool)
|
||||
for _, feed := range feedRows {
|
||||
entry, ok := costMap[feed.ProjectFlockKandangID]
|
||||
if !ok {
|
||||
rows = append(rows, HppPerKandangCostRow{
|
||||
ProjectFlockKandangID: feed.ProjectFlockKandangID,
|
||||
})
|
||||
entry = &rows[len(rows)-1]
|
||||
costMap[feed.ProjectFlockKandangID] = entry
|
||||
}
|
||||
entry.FeedCost += feed.FeedCost
|
||||
if feed.SupplierID != nil {
|
||||
if feedSeen[feed.ProjectFlockKandangID] == nil {
|
||||
feedSeen[feed.ProjectFlockKandangID] = make(map[uint]bool)
|
||||
}
|
||||
if !feedSeen[feed.ProjectFlockKandangID][*feed.SupplierID] {
|
||||
feedSeen[feed.ProjectFlockKandangID][*feed.SupplierID] = true
|
||||
supplierName := ""
|
||||
if feed.SupplierName != nil {
|
||||
supplierName = *feed.SupplierName
|
||||
}
|
||||
supplierAlias := ""
|
||||
if feed.SupplierAlias != nil {
|
||||
supplierAlias = *feed.SupplierAlias
|
||||
}
|
||||
feedSuppliers = append(feedSuppliers, HppPerKandangSupplierRow{
|
||||
ProjectFlockKandangID: feed.ProjectFlockKandangID,
|
||||
SupplierID: *feed.SupplierID,
|
||||
SupplierName: supplierName,
|
||||
SupplierAlias: supplierAlias,
|
||||
Category: "FEED",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
docSuppliers = append(docSuppliers, feedSuppliers...)
|
||||
return rows, docSuppliers, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,25 @@ func (r *productionResultRepositoryImpl) GetRecordingsByProjectFlockKandang(
|
||||
return []entity.Recording{}, 0, nil
|
||||
}
|
||||
|
||||
latestApproval := r.db.WithContext(ctx).
|
||||
Table("approvals AS a").
|
||||
Select("a.approvable_id, a.action, a.step_number").
|
||||
Joins(`
|
||||
JOIN (
|
||||
SELECT approvable_id, MAX(action_at) AS latest_action_at
|
||||
FROM approvals
|
||||
WHERE approvable_type = ?
|
||||
GROUP BY approvable_id
|
||||
) AS la ON la.approvable_id = a.approvable_id AND la.latest_action_at = a.action_at`,
|
||||
string(utils.ApprovalWorkflowRecording),
|
||||
)
|
||||
|
||||
countQuery := r.db.WithContext(ctx).
|
||||
Model(&entity.Recording{}).
|
||||
Where("project_flock_kandangs_id = ?", projectFlockKandangID)
|
||||
Joins("JOIN (?) AS la ON la.approvable_id = recordings.id", latestApproval).
|
||||
Where("project_flock_kandangs_id = ?", projectFlockKandangID).
|
||||
Where("la.step_number = ?", utils.RecordingStepDisetujui).
|
||||
Where("la.action = ?", string(entity.ApprovalActionApproved))
|
||||
|
||||
var total int64
|
||||
if err := countQuery.Count(&total).Error; err != nil {
|
||||
@@ -59,7 +75,10 @@ func (r *productionResultRepositoryImpl) GetRecordingsByProjectFlockKandang(
|
||||
|
||||
dataQuery := r.db.WithContext(ctx).
|
||||
Model(&entity.Recording{}).
|
||||
Joins("JOIN (?) AS la ON la.approvable_id = recordings.id", latestApproval).
|
||||
Where("project_flock_kandangs_id = ?", projectFlockKandangID).
|
||||
Where("la.step_number = ?", utils.RecordingStepDisetujui).
|
||||
Where("la.action = ?", string(entity.ApprovalActionApproved)).
|
||||
Preload("Eggs", func(db *gorm.DB) *gorm.DB {
|
||||
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").
|
||||
|
||||
@@ -398,6 +398,9 @@ func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.
|
||||
if detail != nil && detail.TargetMeanBw != nil {
|
||||
weeklyResults[i].StdBw = *detail.TargetMeanBw
|
||||
}
|
||||
if detail != nil {
|
||||
weeklyResults[i].DepStd = valueOrZero(detail.MaxDepletion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,6 +582,11 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
|
||||
return dto.CustomerPaymentReportItem{}, err
|
||||
}
|
||||
|
||||
filterBy := strings.ToUpper(strings.TrimSpace(params.FilterBy))
|
||||
if filterBy == "" {
|
||||
filterBy = utils.CustomerPaymentFilterByTransDate
|
||||
}
|
||||
|
||||
var startDate, endDate *time.Time
|
||||
if params.StartDate != "" {
|
||||
parsed, err := time.ParseInLocation("2006-01-02", params.StartDate, location)
|
||||
@@ -597,11 +605,20 @@ func (s *repportService) processCustomerPayment(ctx context.Context, customerID
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
transDate := row.TransDate.In(location)
|
||||
if startDate != nil && transDate.Before(*startDate) {
|
||||
var compareDate time.Time
|
||||
if filterBy == utils.CustomerPaymentFilterByRealizationDate {
|
||||
if row.DeliveryDate == nil {
|
||||
continue
|
||||
}
|
||||
compareDate = row.DeliveryDate.In(location)
|
||||
} else {
|
||||
compareDate = row.TransDate.In(location)
|
||||
}
|
||||
|
||||
if startDate != nil && compareDate.Before(*startDate) {
|
||||
continue
|
||||
}
|
||||
if endDate != nil && transDate.After(*endDate) {
|
||||
if endDate != nil && compareDate.After(*endDate) {
|
||||
continue
|
||||
}
|
||||
filteredRows = append(filteredRows, row)
|
||||
@@ -1352,10 +1369,12 @@ func buildDebtSupplierRow(purchase entity.Purchase, now time.Time, loc *time.Loc
|
||||
poNumber = *purchase.PoNumber
|
||||
}
|
||||
|
||||
prDate := purchase.CreatedAt.In(loc)
|
||||
startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc)
|
||||
startDate := resolveDebtSupplierReceivedDate(purchase, loc)
|
||||
endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
||||
aging := int(endDate.Sub(startDate).Hours() / 24)
|
||||
aging := 0
|
||||
if !startDate.IsZero() {
|
||||
aging = int(endDate.Sub(startDate).Hours() / 24)
|
||||
}
|
||||
|
||||
totalPrice := 0.0
|
||||
travelNumber := "-"
|
||||
@@ -1525,8 +1544,10 @@ func isDebtSupplierPaid(totalPrice, paymentTotal float64) bool {
|
||||
}
|
||||
|
||||
func calculateDebtSupplierAging(purchase entity.Purchase, endDate time.Time, loc *time.Location) int {
|
||||
prDate := purchase.CreatedAt.In(loc)
|
||||
startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc)
|
||||
startDate := resolveDebtSupplierReceivedDate(purchase, loc)
|
||||
if startDate.IsZero() {
|
||||
return 0
|
||||
}
|
||||
stopDate := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, loc)
|
||||
if stopDate.Before(startDate) {
|
||||
return 0
|
||||
@@ -1534,6 +1555,23 @@ func calculateDebtSupplierAging(purchase entity.Purchase, endDate time.Time, loc
|
||||
return int(stopDate.Sub(startDate).Hours() / 24)
|
||||
}
|
||||
|
||||
func resolveDebtSupplierReceivedDate(purchase entity.Purchase, loc *time.Location) time.Time {
|
||||
earliest := time.Time{}
|
||||
for _, item := range purchase.Items {
|
||||
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
|
||||
continue
|
||||
}
|
||||
received := item.ReceivedDate.In(loc)
|
||||
if earliest.IsZero() || received.Before(earliest) {
|
||||
earliest = received
|
||||
}
|
||||
}
|
||||
if earliest.IsZero() {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Date(earliest.Year(), earliest.Month(), earliest.Day(), 0, 0, 0, 0, loc)
|
||||
}
|
||||
|
||||
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
|
||||
params, filters, err := s.parseHppPerKandangQuery(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -85,6 +85,7 @@ type CustomerPaymentQuery struct {
|
||||
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
|
||||
CustomerIDs []uint `query:"customer_ids" validate:"omitempty,dive,gt=0"`
|
||||
FilterBy string `query:"filter_by" validate:"omitempty,oneof=TRANS_DATE REALIZATION_DATE"`
|
||||
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
}
|
||||
|
||||
@@ -161,6 +161,15 @@ const (
|
||||
ExpenseCategoryNonBOP ExpenseCategory = "NON-BOP"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Filter Customer Payment
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const (
|
||||
CustomerPaymentFilterByTransDate = "TRANS_DATE"
|
||||
CustomerPaymentFilterByRealizationDate = "REALIZATION_DATE"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Payment Method
|
||||
// -------------------------------------------------------------------
|
||||
@@ -354,9 +363,10 @@ const (
|
||||
PurchaseStepReceiving approvalutils.ApprovalStep = 4
|
||||
PurchaseStepCompleted approvalutils.ApprovalStep = 5
|
||||
|
||||
PurchasePRNumberPrefix = "PR-LTI-"
|
||||
PurchasePONumberPrefix = "PO-LTI-"
|
||||
PurchaseNumberPadding = 4
|
||||
PurchasePRNumberPrefix = "PR-LTI-"
|
||||
PurchasePONumberPrefix = "PO-LTI-"
|
||||
AdjustmentStockNumberPrefix = "ADJ-"
|
||||
PurchaseNumberPadding = 4
|
||||
)
|
||||
|
||||
var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||
|
||||
@@ -17,4 +17,5 @@ const (
|
||||
StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS"
|
||||
StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION"
|
||||
StockableKeyRecordingEgg StockableKey = "RECORDING_EGG"
|
||||
StockableKeyRecordingDepletion StockableKey = "RECORDING_DEPLETION_IN"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
package recording
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type warnLogger interface {
|
||||
Warnf(format string, args ...any)
|
||||
}
|
||||
|
||||
type productWarehouseExistsRepo interface {
|
||||
ExistsByID(ctx context.Context, id uint) (bool, error)
|
||||
}
|
||||
|
||||
type recordingValidationRepo interface {
|
||||
ValidateProductWarehousesByFlags(ctx context.Context, ids []uint, flags []string) (uint, error)
|
||||
}
|
||||
|
||||
func EnsureProductWarehousesExist(ctx context.Context, repo productWarehouseExistsRepo, ids []uint) error {
|
||||
if repo == nil || len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, id := range ids {
|
||||
ok, err := repo.ExistsByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("product warehouse %d not found", id)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnsureProductWarehousesByFlags(ctx context.Context, repo recordingValidationRepo, ids []uint, flags []string, label string) error {
|
||||
if repo == nil || len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
invalidID, err := repo.ValidateProductWarehousesByFlags(ctx, ids, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if invalidID != 0 {
|
||||
return fmt.Errorf("product warehouse %d is not a %s warehouse", invalidID, label)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type idGetter[T any] func(T) uint
|
||||
|
||||
func CollectWarehouseIDs[T any](items []T, getID idGetter[T]) []uint {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
ids := make([]uint, 0, len(items))
|
||||
for _, item := range items {
|
||||
if id := getID(item); id != 0 {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func EnsureProductWarehousesByFlagsForItems[T any](
|
||||
ctx context.Context,
|
||||
repo recordingValidationRepo,
|
||||
items []T,
|
||||
getID idGetter[T],
|
||||
flags []string,
|
||||
label string,
|
||||
) error {
|
||||
ids := CollectWarehouseIDs(items, getID)
|
||||
return EnsureProductWarehousesByFlags(ctx, repo, ids, flags, label)
|
||||
}
|
||||
|
||||
func ComputeDepletionRate(prevRecording *entity.Recording, currentDepletion float64, totalChick int64) float64 {
|
||||
base := 0.0
|
||||
if prevRecording != nil && prevRecording.TotalChickQty != nil && *prevRecording.TotalChickQty > 0 {
|
||||
base = *prevRecording.TotalChickQty
|
||||
} else if totalChick > 0 {
|
||||
base = float64(totalChick) + currentDepletion
|
||||
}
|
||||
if base <= 0 {
|
||||
return 0
|
||||
}
|
||||
return (currentDepletion / base) * 100
|
||||
}
|
||||
|
||||
func AttachLatestApprovals(ctx context.Context, items []entity.Recording, approvalSvc commonSvc.ApprovalService, logger warnLogger) error {
|
||||
if len(items) == 0 || approvalSvc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ids := make([]uint, 0, len(items))
|
||||
visited := make(map[uint]struct{}, len(items))
|
||||
for _, item := range items {
|
||||
if item.Id == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := visited[item.Id]; ok {
|
||||
continue
|
||||
}
|
||||
visited[item.Id] = struct{}{}
|
||||
ids = append(ids, item.Id)
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
latestMap, err := approvalSvc.LatestByTargets(ctx, utils.ApprovalWorkflowRecording, ids, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("ActionUser")
|
||||
})
|
||||
if err != nil {
|
||||
if logger != nil {
|
||||
logger.Warnf("Unable to load latest approvals for recordings: %+v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(latestMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := range items {
|
||||
if items[i].Id == 0 {
|
||||
continue
|
||||
}
|
||||
if approval, ok := latestMap[items[i].Id]; ok {
|
||||
items[i].LatestApproval = approval
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func AttachLatestApproval(ctx context.Context, item *entity.Recording, approvalSvc commonSvc.ApprovalService, logger warnLogger) error {
|
||||
if item == nil || item.Id == 0 || approvalSvc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
latest, err := approvalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowRecording, item.Id, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("ActionUser")
|
||||
})
|
||||
if err != nil {
|
||||
if logger != nil {
|
||||
logger.Warnf("Unable to load approvals for recording %d: %+v", item.Id, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
item.LatestApproval = latest
|
||||
return nil
|
||||
}
|
||||
|
||||
type productionStandardValues struct {
|
||||
HenDay *float64
|
||||
HenHouse *float64
|
||||
FeedIntake *float64
|
||||
MaxDepletion *float64
|
||||
EggMass *float64
|
||||
EggWeight *float64
|
||||
}
|
||||
|
||||
func AttachProductionStandards(ctx context.Context, db *gorm.DB, warnOnly bool, logger warnLogger, items ...*entity.Recording) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type standardKey struct {
|
||||
standardID uint
|
||||
week int
|
||||
}
|
||||
type standardCacheEntry struct {
|
||||
values productionStandardValues
|
||||
fcr *float64
|
||||
}
|
||||
|
||||
if db == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
standardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
|
||||
growthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
|
||||
cache := make(map[standardKey]standardCacheEntry, len(items))
|
||||
|
||||
standardIDs := make(map[uint]struct{}, len(items))
|
||||
for _, item := range items {
|
||||
if item == nil || item.ProjectFlockKandang == nil || item.ProjectFlockKandang.ProjectFlock.Id == 0 {
|
||||
continue
|
||||
}
|
||||
if item.ProjectFlockKandang.ProjectFlock.ProductionStandardId > 0 {
|
||||
standardIDs[item.ProjectFlockKandang.ProjectFlock.ProductionStandardId] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
standardDetailByStd := make(map[uint]map[int]*entity.ProductionStandardDetail, len(standardIDs))
|
||||
growthDetailByStd := make(map[uint]map[int]*entity.StandardGrowthDetail, len(standardIDs))
|
||||
|
||||
for standardID := range standardIDs {
|
||||
details, err := standardDetailRepo.GetByProductionStandardID(ctx, standardID)
|
||||
if err != nil {
|
||||
if warnOnly {
|
||||
if logger != nil {
|
||||
logger.Warnf("Unable to preload production standard detail for standard %d: %+v", standardID, err)
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
detailMap := make(map[int]*entity.ProductionStandardDetail, len(details))
|
||||
for i := range details {
|
||||
detail := details[i]
|
||||
detailMap[detail.Week] = &detail
|
||||
}
|
||||
standardDetailByStd[standardID] = detailMap
|
||||
|
||||
growths, err := growthDetailRepo.GetByProductionStandardID(ctx, standardID)
|
||||
if err != nil {
|
||||
if warnOnly {
|
||||
if logger != nil {
|
||||
logger.Warnf("Unable to preload standard growth detail for standard %d: %+v", standardID, err)
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
growthMap := make(map[int]*entity.StandardGrowthDetail, len(growths))
|
||||
for i := range growths {
|
||||
growth := growths[i]
|
||||
growthMap[growth.Week] = &growth
|
||||
}
|
||||
growthDetailByStd[standardID] = growthMap
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if item == nil || item.ProjectFlockKandang == nil || item.ProjectFlockKandang.ProjectFlock.Id == 0 {
|
||||
continue
|
||||
}
|
||||
standardID := item.ProjectFlockKandang.ProjectFlock.ProductionStandardId
|
||||
if standardID == 0 {
|
||||
continue
|
||||
}
|
||||
week := RecordingWeekValue(*item)
|
||||
cacheKey := standardKey{standardID: standardID, week: week}
|
||||
if cached, ok := cache[cacheKey]; ok {
|
||||
applyProductionStandardValues(item, cached.values, cached.fcr)
|
||||
continue
|
||||
}
|
||||
values := productionStandardValues{}
|
||||
var fcr *float64
|
||||
if detailMap, ok := standardDetailByStd[standardID]; ok {
|
||||
if detail, ok := detailMap[week]; ok {
|
||||
values.HenDay = detail.TargetHenDayProduction
|
||||
values.HenHouse = detail.TargetHenHouseProduction
|
||||
values.EggMass = detail.TargetEggMass
|
||||
values.EggWeight = detail.TargetEggWeight
|
||||
fcr = detail.StandardFCR
|
||||
}
|
||||
}
|
||||
if growthMap, ok := growthDetailByStd[standardID]; ok {
|
||||
if growth, ok := growthMap[week]; ok {
|
||||
values.FeedIntake = growth.FeedIntake
|
||||
values.MaxDepletion = growth.MaxDepletion
|
||||
}
|
||||
}
|
||||
cache[cacheKey] = standardCacheEntry{values: values, fcr: fcr}
|
||||
applyProductionStandardValues(item, values, fcr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyProductionStandardValues(item *entity.Recording, values productionStandardValues, fcr *float64) {
|
||||
item.StandardHenDay = values.HenDay
|
||||
item.StandardHenHouse = values.HenHouse
|
||||
item.StandardFeedIntake = values.FeedIntake
|
||||
item.StandardMaxDepletion = values.MaxDepletion
|
||||
item.StandardEggMass = values.EggMass
|
||||
item.StandardEggWeight = values.EggWeight
|
||||
item.StandardFcr = fcr
|
||||
}
|
||||
|
||||
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 intValue(value *int) int {
|
||||
if value == nil {
|
||||
return 0
|
||||
}
|
||||
return *value
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package recording
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
||||
)
|
||||
@@ -28,12 +31,26 @@ func MapDepletions(recordingID uint, items []validation.Depletion) []entity.Reco
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]entity.RecordingDepletion, 0, len(items))
|
||||
aggregate := make(map[uint]float64, len(items))
|
||||
for _, item := range items {
|
||||
if item.ProductWarehouseId == 0 || item.Qty == 0 {
|
||||
continue
|
||||
}
|
||||
aggregate[item.ProductWarehouseId] += item.Qty
|
||||
}
|
||||
if len(aggregate) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]entity.RecordingDepletion, 0, len(aggregate))
|
||||
for warehouseID, qty := range aggregate {
|
||||
if qty == 0 {
|
||||
continue
|
||||
}
|
||||
result = append(result, entity.RecordingDepletion{
|
||||
RecordingId: recordingID,
|
||||
ProductWarehouseId: item.ProductWarehouseId,
|
||||
Qty: item.Qty,
|
||||
ProductWarehouseId: warehouseID,
|
||||
Qty: qty,
|
||||
})
|
||||
}
|
||||
return result
|
||||
@@ -56,3 +73,87 @@ func MapEggs(recordingID uint, createdBy uint, items []validation.Egg) []entity.
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type EggTotals struct {
|
||||
Qty int
|
||||
Weight float64
|
||||
}
|
||||
|
||||
func StockUsageByWarehouse(items []entity.RecordingStock) map[uint]float64 {
|
||||
return TotalsByWarehouse(items, func(stock entity.RecordingStock) (uint, float64) {
|
||||
var usage float64
|
||||
if stock.UsageQty != nil {
|
||||
usage = *stock.UsageQty
|
||||
}
|
||||
return stock.ProductWarehouseId, usage
|
||||
})
|
||||
}
|
||||
|
||||
func StockUsageByWarehouseReq(items []validation.Stock) map[uint]float64 {
|
||||
return TotalsByWarehouse(items, func(item validation.Stock) (uint, float64) {
|
||||
return item.ProductWarehouseId, item.Qty
|
||||
})
|
||||
}
|
||||
|
||||
func FloatMapsEqual(a, b map[uint]float64) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for key, value := range a {
|
||||
other, ok := b[key]
|
||||
if !ok || !floatNearlyEqual(value, other) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func EggTotalsEqual(a, b map[uint]EggTotals) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for key, value := range a {
|
||||
other, ok := b[key]
|
||||
if !ok || value.Qty != other.Qty || !floatNearlyEqual(value.Weight, other.Weight) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func floatNearlyEqual(a, b float64) bool {
|
||||
return a-b <= 0.000001 && b-a <= 0.000001
|
||||
}
|
||||
|
||||
func TotalsByWarehouse[T any](items []T, get func(T) (uint, float64)) map[uint]float64 {
|
||||
result := make(map[uint]float64)
|
||||
for _, item := range items {
|
||||
warehouseID, qty := get(item)
|
||||
result[warehouseID] += qty
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func EggTotalsByWarehouse[T any](items []T, get func(T) (uint, int, *float64)) map[uint]EggTotals {
|
||||
result := make(map[uint]EggTotals)
|
||||
for _, item := range items {
|
||||
warehouseID, qty, weightPtr := get(item)
|
||||
weight := 0.0
|
||||
if weightPtr != nil {
|
||||
weight = *weightPtr
|
||||
}
|
||||
current := result[warehouseID]
|
||||
current.Qty += qty
|
||||
current.Weight += weight
|
||||
result[warehouseID] = current
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func RecordingNote(action string, id uint) string {
|
||||
action = strings.TrimSpace(action)
|
||||
if action == "" {
|
||||
return fmt.Sprintf("Recording#%d", id)
|
||||
}
|
||||
return fmt.Sprintf("Recording-%s#%d", action, id)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user