mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e52c51987 | |||
| c2b60c1aff | |||
| 02cc082d67 | |||
| 69469edb62 | |||
| 0708628b78 | |||
| 11f2389ec5 | |||
| 26f9196876 | |||
| 17d3042586 | |||
| 80c84210b8 | |||
| 05ec64b456 | |||
| 8f74391f1e | |||
| 4aed480662 | |||
| e5b91161a9 | |||
| a38491fef1 | |||
| b234778634 | |||
| 59e71856ac | |||
| 1ee97b91a5 | |||
| 3a5c49c511 | |||
| 48730e1b74 | |||
| f97d404121 | |||
| 8220e34302 | |||
| c72db5bd18 | |||
| 86f37a89c1 | |||
| 20f1be2ef8 | |||
| 672c76d26d | |||
| 219a6a39ed | |||
| c91d84b652 | |||
| bf14ab7865 | |||
| 31bb28f7da | |||
| a390d1d23a |
@@ -1,78 +0,0 @@
|
|||||||
stages:
|
|
||||||
- build
|
|
||||||
- deploy
|
|
||||||
- cleanup
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 🏗️ BUILD IMAGE (Overwrite :dev)
|
|
||||||
# ==============================
|
|
||||||
build_image:
|
|
||||||
stage: build
|
|
||||||
image: docker:latest
|
|
||||||
services:
|
|
||||||
- docker:dind
|
|
||||||
variables:
|
|
||||||
DOCKER_TLS_CERTDIR: "/certs"
|
|
||||||
script:
|
|
||||||
- echo "🔧 Building Docker image for :dev..."
|
|
||||||
- docker login -u gitlab-ci-token -p "$CI_JOB_TOKEN" "$CI_REGISTRY"
|
|
||||||
- docker build -f Dockerfile.local -t registry.gitlab.com/mbugroup/sso-mbugroup/lti-api:dev .
|
|
||||||
- docker push registry.gitlab.com/mbugroup/sso-mbugroup/lti-api:dev
|
|
||||||
only:
|
|
||||||
- development
|
|
||||||
|
|
||||||
# ==============================
|
|
||||||
# 🚀 DEPLOY TO DEV SERVER
|
|
||||||
# ==============================
|
|
||||||
deploy_lti:
|
|
||||||
stage: deploy
|
|
||||||
image: alpine:latest
|
|
||||||
before_script:
|
|
||||||
- apk add --no-cache openssh-client bash curl
|
|
||||||
- mkdir -p ~/.ssh
|
|
||||||
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
|
||||||
- chmod 600 ~/.ssh/id_rsa
|
|
||||||
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
|
|
||||||
|
|
||||||
script:
|
|
||||||
- echo "🚀 Deploy ke ${SERVER_USER}@${SERVER_IP} menggunakan image :dev"
|
|
||||||
- |
|
|
||||||
ssh -o StrictHostKeyChecking=no ${SERVER_USER}@${SERVER_IP} bash -s <<REMOTE
|
|
||||||
set -e
|
|
||||||
|
|
||||||
APP_NAME="lti-api"
|
|
||||||
DOCKER_IMAGE="registry.gitlab.com/mbugroup/sso-mbugroup/lti-api:dev"
|
|
||||||
NETWORK_NAME="lti-network"
|
|
||||||
ENV_PATH="/home/devops/code/api/lti-api/.env.lti-api"
|
|
||||||
PORT=8081
|
|
||||||
|
|
||||||
echo "🔑 Login ke GitLab Registry..."
|
|
||||||
echo "${GITLAB_TOKEN}" | docker login -u "${GITLAB_USER}" --password-stdin registry.gitlab.com
|
|
||||||
|
|
||||||
echo "🛑 Stop & remove old container..."
|
|
||||||
docker stop "\${APP_NAME}" >/dev/null 2>&1 || true
|
|
||||||
docker rm -f "\${APP_NAME}" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
echo "🧹 Membersihkan container zombie di port \${PORT}..."
|
|
||||||
OLD_ID=\$(docker ps -aq --filter "publish=\${PORT}")
|
|
||||||
if [ -n "\${OLD_ID}" ]; then
|
|
||||||
echo "⚠️ Container lain masih pakai port \${PORT}, hapus..."
|
|
||||||
docker stop \${OLD_ID} >/dev/null 2>&1 || true
|
|
||||||
docker rm -f \${OLD_ID} >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "🐳 Pull image baru..."
|
|
||||||
docker pull "\${DOCKER_IMAGE}"
|
|
||||||
|
|
||||||
echo "🚀 Run container baru..."
|
|
||||||
docker run -d --name "\${APP_NAME}" --restart always \
|
|
||||||
--env-file "\${ENV_PATH}" \
|
|
||||||
-p \${PORT}:8081 \
|
|
||||||
--network "\${NETWORK_NAME}" \
|
|
||||||
"\${DOCKER_IMAGE}"
|
|
||||||
|
|
||||||
echo "✅ Deployment selesai di port \${PORT}"
|
|
||||||
REMOTE
|
|
||||||
|
|
||||||
only:
|
|
||||||
- development
|
|
||||||
@@ -13,6 +13,7 @@ type ApprovalRepository interface {
|
|||||||
FindByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
|
FindByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
|
||||||
LatestByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
|
LatestByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
|
||||||
LatestByTargets(ctx context.Context, workflow string, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]entity.Approval, error)
|
LatestByTargets(ctx context.Context, workflow string, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]entity.Approval, error)
|
||||||
|
DeleteByTarget(ctx context.Context, workflow string, approvableID uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type approvalRepositoryImpl struct {
|
type approvalRepositoryImpl struct {
|
||||||
@@ -104,3 +105,13 @@ func (r *approvalRepositoryImpl) LatestByTargets(
|
|||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *approvalRepositoryImpl) DeleteByTarget(
|
||||||
|
ctx context.Context,
|
||||||
|
workflow string,
|
||||||
|
approvableID uint,
|
||||||
|
) error {
|
||||||
|
return r.DB().WithContext(ctx).
|
||||||
|
Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID).
|
||||||
|
Delete(&entity.Approval{}).Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package validation
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
)
|
)
|
||||||
@@ -21,34 +23,41 @@ var customMessages = map[string]string{
|
|||||||
"alphanum": "Field %s must contain only alphanumeric characters",
|
"alphanum": "Field %s must contain only alphanumeric characters",
|
||||||
"oneof": "Invalid value for field %s",
|
"oneof": "Invalid value for field %s",
|
||||||
"password": "Field %s must be at least 8 characters, contain uppercase, lowercase, number, and special character",
|
"password": "Field %s must be at least 8 characters, contain uppercase, lowercase, number, and special character",
|
||||||
|
"gt": "Invalid %s, must be greater than %s",
|
||||||
}
|
}
|
||||||
|
|
||||||
func CustomErrorMessages(err error) map[string]string {
|
func CustomErrorMessages(err error) (string, map[string]string) {
|
||||||
var validationErrors validator.ValidationErrors
|
var validationErrors validator.ValidationErrors
|
||||||
if errors.As(err, &validationErrors) {
|
if errors.As(err, &validationErrors) {
|
||||||
return generateErrorMessages(validationErrors)
|
return generateErrorMessages(validationErrors)
|
||||||
}
|
}
|
||||||
return nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateErrorMessages(validationErrors validator.ValidationErrors) map[string]string {
|
func generateErrorMessages(validationErrors validator.ValidationErrors) (string, map[string]string) {
|
||||||
errorsMap := make(map[string]string)
|
errorsMap := make(map[string]string)
|
||||||
for _, err := range validationErrors {
|
var firstMessage string
|
||||||
|
for i, err := range validationErrors {
|
||||||
fieldName := err.StructNamespace()
|
fieldName := err.StructNamespace()
|
||||||
tag := err.Tag()
|
tag := err.Tag()
|
||||||
|
|
||||||
customMessage := customMessages[tag]
|
customMessage := customMessages[tag]
|
||||||
|
var msg string
|
||||||
if customMessage != "" {
|
if customMessage != "" {
|
||||||
errorsMap[fieldName] = formatErrorMessage(customMessage, err, tag)
|
msg = formatErrorMessage(customMessage, err, tag)
|
||||||
} else {
|
} else {
|
||||||
errorsMap[fieldName] = defaultErrorMessage(err)
|
msg = defaultErrorMessage(err)
|
||||||
|
}
|
||||||
|
errorsMap[fieldName] = msg
|
||||||
|
if i == 0 {
|
||||||
|
firstMessage = msg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return errorsMap
|
return firstMessage, errorsMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string {
|
func formatErrorMessage(customMessage string, err validator.FieldError, tag string) string {
|
||||||
if tag == "min" || tag == "max" || tag == "len" {
|
if tag == "min" || tag == "max" || tag == "len" || tag == "gt" {
|
||||||
return fmt.Sprintf(customMessage, err.Field(), err.Param())
|
return fmt.Sprintf(customMessage, err.Field(), err.Param())
|
||||||
}
|
}
|
||||||
return fmt.Sprintf(customMessage, err.Field())
|
return fmt.Sprintf(customMessage, err.Field())
|
||||||
@@ -61,6 +70,16 @@ func defaultErrorMessage(err validator.FieldError) string {
|
|||||||
func Validator() *validator.Validate {
|
func Validator() *validator.Validate {
|
||||||
validate := validator.New()
|
validate := validator.New()
|
||||||
|
|
||||||
|
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
|
||||||
|
if jsonTag := getTagName(fld, "json"); jsonTag != "" {
|
||||||
|
return jsonTag
|
||||||
|
}
|
||||||
|
if queryTag := getTagName(fld, "query"); queryTag != "" {
|
||||||
|
return queryTag
|
||||||
|
}
|
||||||
|
return fld.Name
|
||||||
|
})
|
||||||
|
|
||||||
if err := validate.RegisterValidation("password", Password); err != nil {
|
if err := validate.RegisterValidation("password", Password); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -72,3 +91,16 @@ func Validator() *validator.Validate {
|
|||||||
}
|
}
|
||||||
return validate
|
return validate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTagName(fld reflect.StructField, tag string) string {
|
||||||
|
value, ok := fld.Tag.Lookup(tag)
|
||||||
|
if !ok || value == "-" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
name := strings.Split(value, ",")[0]
|
||||||
|
if name == "" || name == "-" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
@@ -1,36 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS project_chickins (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
project_flock_kandang_id BIGINT NOT NULL,
|
|
||||||
chick_in_date DATE NOT NULL,
|
|
||||||
quantity NUMERIC(15, 3) NOT NULL,
|
|
||||||
note TEXT,
|
|
||||||
created_by BIGINT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
deleted_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
|
||||||
ALTER TABLE project_chickins
|
|
||||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
|
||||||
FOREIGN KEY (project_flock_kandang_id)
|
|
||||||
REFERENCES project_flock_kandangs(id)
|
|
||||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
|
||||||
ALTER TABLE project_chickins
|
|
||||||
ADD CONSTRAINT fk_created_by
|
|
||||||
FOREIGN KEY (created_by)
|
|
||||||
REFERENCES users(id)
|
|
||||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- INDEXES
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_chickins_project_flock_kandang_id ON project_chickins (project_flock_kandang_id);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_chickins_created_by ON project_chickins (created_by);
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS project_flock_populations (
|
|
||||||
id BIGSERIAL PRIMARY KEY,
|
|
||||||
project_flock_kandang_id BIGINT NOT NULL,
|
|
||||||
initial_quantity NUMERIC(15, 3) NOT NULL,
|
|
||||||
current_quantity NUMERIC(15, 3) NOT NULL,
|
|
||||||
reserved_quantity NUMERIC(15, 3),
|
|
||||||
created_by BIGINT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT now(),
|
|
||||||
deleted_at TIMESTAMPTZ
|
|
||||||
);
|
|
||||||
|
|
||||||
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
|
||||||
ALTER TABLE project_flock_populations
|
|
||||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
|
||||||
FOREIGN KEY (project_flock_kandang_id)
|
|
||||||
REFERENCES project_flock_kandangs(id)
|
|
||||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
|
||||||
ALTER TABLE project_flock_populations
|
|
||||||
ADD CONSTRAINT fk_created_by
|
|
||||||
FOREIGN KEY (created_by)
|
|
||||||
REFERENCES users(id)
|
|
||||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
-- INDEXES
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_project_flock_kandang_id ON project_flock_populations (project_flock_kandang_id);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_project_flock_populations_created_by ON project_flock_populations (created_by);
|
|
||||||
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS project_chickin_details (
|
|||||||
deleted_at TIMESTAMPTZ
|
deleted_at TIMESTAMPTZ
|
||||||
);
|
);
|
||||||
|
|
||||||
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
|
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
|
||||||
|
|||||||
+26
-18
@@ -1,22 +1,30 @@
|
|||||||
|
|
||||||
ALTER TABLE kandangs
|
ALTER TABLE kandangs
|
||||||
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
|
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
|
||||||
|
|
||||||
ALTER TABLE kandangs
|
ALTER TABLE kandangs DROP COLUMN IF EXISTS project_flock_id;
|
||||||
DROP COLUMN IF EXISTS project_flock_id;
|
|
||||||
|
|
||||||
ALTER TABLE project_chickins
|
-- Only alter if tables exist
|
||||||
DROP CONSTRAINT fk_project_flock_kandang_id,
|
DO $$
|
||||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
BEGIN
|
||||||
FOREIGN KEY (project_flock_kandang_id)
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
|
||||||
REFERENCES project_flock_kandangs(id)
|
ALTER TABLE project_chickins
|
||||||
ON UPDATE CASCADE
|
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
|
||||||
ON DELETE CASCADE;
|
ALTER TABLE project_chickins
|
||||||
|
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs(id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
ALTER TABLE project_flock_populations
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_populations') THEN
|
||||||
DROP CONSTRAINT fk_project_flock_kandang_id,
|
ALTER TABLE project_flock_populations
|
||||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
|
||||||
FOREIGN KEY (project_flock_kandang_id)
|
ALTER TABLE project_flock_populations
|
||||||
REFERENCES project_flock_kandangs(id)
|
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||||
ON UPDATE CASCADE
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
ON DELETE CASCADE;
|
REFERENCES project_flock_kandangs(id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS laying_transfers CASCADE;
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS laying_transfers (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
transfer_number VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
from_project_flock_id BIGINT NOT NULL,
|
||||||
|
to_project_flock_id BIGINT NOT NULL,
|
||||||
|
transfer_date DATE NOT NULL,
|
||||||
|
pending_usage_qty NUMERIC(15, 3),
|
||||||
|
usage_qty NUMERIC(15, 3),
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
created_by BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_from_project_flock
|
||||||
|
FOREIGN KEY (from_project_flock_id)
|
||||||
|
REFERENCES project_flocks(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_to_project_flock
|
||||||
|
FOREIGN KEY (to_project_flock_id)
|
||||||
|
REFERENCES project_flocks(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
ALTER TABLE laying_transfers
|
||||||
|
ADD CONSTRAINT fk_laying_created_by
|
||||||
|
FOREIGN KEY (created_by)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- INDEXES
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_laying_transfers_transfer_number ON laying_transfers (transfer_number)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_from_project_flock_id ON laying_transfers (from_project_flock_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers (to_project_flock_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_created_by ON laying_transfers (created_by);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_laying_transfers_deleted_at ON laying_transfers (deleted_at);
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- MIGRATION: project_chickins
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- STEP 1: Hapus tabel jika sudah ada
|
||||||
|
DROP TABLE IF EXISTS project_chickins;
|
||||||
|
|
||||||
|
-- STEP 2: Buat tabel project_chickins
|
||||||
|
CREATE TABLE IF NOT EXISTS project_chickins (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
project_flock_kandang_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT NOT NULL,
|
||||||
|
chick_in_date DATE NOT NULL,
|
||||||
|
usage_qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
pending_usage_qty NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
notes TEXT,
|
||||||
|
created_by BIGINT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- STEP 3: FOREIGN KEYS
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Relasi ke project_flock_kandangs
|
||||||
|
ALTER TABLE project_chickins
|
||||||
|
ADD CONSTRAINT fk_project_chickins_kandang FOREIGN KEY (project_flock_kandang_id) REFERENCES project_flock_kandangs (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Relasi ke product_warehouses
|
||||||
|
ALTER TABLE project_chickins
|
||||||
|
ADD CONSTRAINT fk_project_chickins_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Relasi ke users
|
||||||
|
ALTER TABLE project_chickins
|
||||||
|
ADD CONSTRAINT fk_project_chickins_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- STEP 4: INDEXES
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chickins_kandang_id ON project_chickins (project_flock_kandang_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chickins_warehouse_id ON project_chickins (product_warehouse_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chickins_created_by ON project_chickins (created_by);
|
||||||
|
|
||||||
|
-- Composite index for common queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chickins_kandang_deleted ON project_chickins (
|
||||||
|
project_flock_kandang_id,
|
||||||
|
deleted_at
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for soft delete queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_chickins_deleted_at ON project_chickins (deleted_at);
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- MIGRATION: project_flock_populations
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- STEP 1: Hapus tabel jika sudah ada
|
||||||
|
DROP TABLE IF EXISTS project_flock_populations;
|
||||||
|
|
||||||
|
-- STEP 2: Buat tabel project_flock_populations
|
||||||
|
CREATE TABLE IF NOT EXISTS project_flock_populations (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
project_chickin_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT NOT NULL,
|
||||||
|
total_qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_used_qty NUMERIC(15, 3) DEFAULT 0,
|
||||||
|
notes TEXT,
|
||||||
|
created_by BIGINT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT now(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- STEP 3: FOREIGN KEYS
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Relasi ke project_chickins
|
||||||
|
ALTER TABLE project_flock_populations
|
||||||
|
ADD CONSTRAINT fk_project_flock_populations_chickin FOREIGN KEY (project_chickin_id) REFERENCES project_chickins (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Relasi ke product_warehouses
|
||||||
|
ALTER TABLE project_flock_populations
|
||||||
|
ADD CONSTRAINT fk_project_flock_populations_warehouse FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Relasi ke users
|
||||||
|
ALTER TABLE project_flock_populations
|
||||||
|
ADD CONSTRAINT fk_project_flock_populations_created_by FOREIGN KEY (created_by) REFERENCES users (id) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- STEP 4: INDEXES
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_populations_chickin_id ON project_flock_populations (project_chickin_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_populations_warehouse_id ON project_flock_populations (product_warehouse_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_populations_created_by ON project_flock_populations (created_by);
|
||||||
|
|
||||||
|
-- Composite index for common queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_populations_chickin_deleted ON project_flock_populations (
|
||||||
|
project_chickin_id,
|
||||||
|
deleted_at
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for soft delete queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_populations_deleted_at ON project_flock_populations (deleted_at);
|
||||||
|
|
||||||
|
-- Unique constraint: one population per chickin
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_populations_chickin_unique ON project_flock_populations (project_chickin_id)
|
||||||
|
WHERE
|
||||||
|
deleted_at IS NULL;
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
-- Rollback laying_transfer_sources dan laying_transfer_targets tables
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS laying_transfer_targets CASCADE;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS laying_transfer_sources CASCADE;
|
||||||
+93
@@ -0,0 +1,93 @@
|
|||||||
|
-- Create laying_transfer_sources dan laying_transfer_targets tables
|
||||||
|
|
||||||
|
-- 1. Create laying_transfer_sources table (detail sumber - kandang asal growing)
|
||||||
|
CREATE TABLE laying_transfer_sources (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
laying_transfer_id BIGINT NOT NULL,
|
||||||
|
source_project_flock_kandang_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT,
|
||||||
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
note TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add foreign keys untuk laying_transfer_sources
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_sources_laying_transfer_id
|
||||||
|
FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_sources_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (source_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
ALTER TABLE laying_transfer_sources
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_sources_product_warehouse_id
|
||||||
|
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 2. Create laying_transfer_targets table (detail tujuan - kandang laying)
|
||||||
|
CREATE TABLE laying_transfer_targets (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
laying_transfer_id BIGINT NOT NULL,
|
||||||
|
target_project_flock_kandang_id BIGINT NOT NULL,
|
||||||
|
qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
product_warehouse_id BIGINT,
|
||||||
|
note TEXT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add foreign keys untuk laying_transfer_targets
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'laying_transfers') THEN
|
||||||
|
ALTER TABLE laying_transfer_targets
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_targets_laying_transfer_id
|
||||||
|
FOREIGN KEY (laying_transfer_id) REFERENCES laying_transfers(id) ON DELETE CASCADE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
|
||||||
|
ALTER TABLE laying_transfer_targets
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_targets_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (target_project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
ALTER TABLE laying_transfer_targets
|
||||||
|
ADD CONSTRAINT fk_laying_transfer_targets_product_warehouse_id
|
||||||
|
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 3. Create indexes untuk laying_transfer_sources
|
||||||
|
CREATE INDEX idx_laying_transfer_sources_laying_transfer_id ON laying_transfer_sources (laying_transfer_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_sources_source_kandang_id ON laying_transfer_sources (
|
||||||
|
source_project_flock_kandang_id
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_sources_product_warehouse_id ON laying_transfer_sources (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_sources_deleted_at ON laying_transfer_sources (deleted_at);
|
||||||
|
|
||||||
|
-- 4. Create indexes untuk laying_transfer_targets
|
||||||
|
CREATE INDEX idx_laying_transfer_targets_laying_transfer_id ON laying_transfer_targets (laying_transfer_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_targets_target_kandang_id ON laying_transfer_targets (
|
||||||
|
target_project_flock_kandang_id
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_targets_product_warehouse_id ON laying_transfer_targets (product_warehouse_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_laying_transfer_targets_deleted_at ON laying_transfer_targets (deleted_at);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS purchase_items;
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS purchase_items (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
purchase_id BIGINT NOT NULL,
|
||||||
|
product_id BIGINT NOT NULL,
|
||||||
|
warehouse_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT,
|
||||||
|
received_date TIMESTAMPTZ,
|
||||||
|
travel_number VARCHAR,
|
||||||
|
travel_number_docs VARCHAR,
|
||||||
|
vehicle_number VARCHAR,
|
||||||
|
sub_qty NUMERIC(15, 3) NOT NULL,
|
||||||
|
total_qty NUMERIC(15, 3) NOT NULL DEFAULT 0,
|
||||||
|
total_used NUMERIC(15, 3) NOT NULL DEFAULT 0,
|
||||||
|
price NUMERIC(15, 3) NOT NULL DEFAULT 0,
|
||||||
|
total_price NUMERIC(15, 3) NOT NULL DEFAULT 0,
|
||||||
|
CONSTRAINT uq_purchase_items_purchase_product_warehouse
|
||||||
|
UNIQUE (purchase_id, product_id, warehouse_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'products') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_product
|
||||||
|
FOREIGN KEY (product_id)
|
||||||
|
REFERENCES products(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'warehouses') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_warehouse
|
||||||
|
FOREIGN KEY (warehouse_id)
|
||||||
|
REFERENCES warehouses(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_product_warehouse
|
||||||
|
FOREIGN KEY (product_warehouse_id)
|
||||||
|
REFERENCES product_warehouses(id)
|
||||||
|
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_id ON purchase_items (product_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_warehouse_id ON purchase_items (warehouse_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_warehouse_id ON purchase_items (product_warehouse_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchase_items_purchase_id ON purchase_items (purchase_id);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_purchase_items_purchase'
|
||||||
|
AND conrelid = 'purchase_items'::regclass
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE purchase_items
|
||||||
|
DROP CONSTRAINT fk_purchase_items_purchase;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS purchases;
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS purchases (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
pr_number VARCHAR NOT NULL,
|
||||||
|
po_number VARCHAR NULL,
|
||||||
|
po_date TIMESTAMPTZ NULL,
|
||||||
|
supplier_id BIGINT NOT NULL,
|
||||||
|
credit_term INT NOT NULL,
|
||||||
|
due_date TIMESTAMPTZ,
|
||||||
|
grand_total NUMERIC(15, 3) NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL,
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
created_by BIGINT NOT NULL,
|
||||||
|
CONSTRAINT uq_purchases_pr_number UNIQUE (pr_number),
|
||||||
|
CONSTRAINT uq_purchases_po_number UNIQUE (po_number)
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchases
|
||||||
|
ADD CONSTRAINT fk_purchases_supplier
|
||||||
|
FOREIGN KEY (supplier_id)
|
||||||
|
REFERENCES suppliers(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchases
|
||||||
|
ADD CONSTRAINT fk_purchases_created_by
|
||||||
|
FOREIGN KEY (created_by)
|
||||||
|
REFERENCES users(id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM pg_tables WHERE tablename = 'purchase_items'
|
||||||
|
) AND NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_purchase_items_purchase'
|
||||||
|
) THEN
|
||||||
|
EXECUTE
|
||||||
|
'ALTER TABLE purchase_items
|
||||||
|
ADD CONSTRAINT fk_purchase_items_purchase
|
||||||
|
FOREIGN KEY (purchase_id)
|
||||||
|
REFERENCES purchases(id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_supplier_id ON purchases (supplier_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_created_by ON purchases (created_by);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_po_date ON purchases (po_date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_purchases_deleted_at ON purchases (deleted_at);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE kandangs
|
||||||
|
DROP COLUMN IF EXISTS capacity;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE kandangs
|
||||||
|
ADD COLUMN capacity NUMERIC(15,3) NOT NULL;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
DROP COLUMN IF EXISTS period;
|
||||||
|
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
ADD COLUMN IF NOT EXISTS period INT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_base_period_unique
|
||||||
|
ON project_flocks (
|
||||||
|
LOWER(TRIM(regexp_replace(flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))),
|
||||||
|
period
|
||||||
|
)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
ADD COLUMN IF NOT EXISTS period INT;
|
||||||
|
|
||||||
|
UPDATE project_flock_kandangs pfk
|
||||||
|
SET period = pf.period
|
||||||
|
FROM project_flocks pf
|
||||||
|
WHERE pfk.project_flock_id = pf.id
|
||||||
|
AND (pfk.period IS NULL OR pfk.period = 0)
|
||||||
|
AND pf.period IS NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
ALTER COLUMN period SET DEFAULT 0;
|
||||||
|
|
||||||
|
UPDATE project_flock_kandangs
|
||||||
|
SET period = 0
|
||||||
|
WHERE period IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
ALTER COLUMN period SET NOT NULL;
|
||||||
|
|
||||||
|
-- Drop period from project_flocks as the source of truth
|
||||||
|
DROP INDEX IF EXISTS project_flocks_base_period_unique;
|
||||||
|
|
||||||
|
ALTER TABLE project_flocks
|
||||||
|
DROP COLUMN IF EXISTS period;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -235,13 +235,14 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
|||||||
seeds := []struct {
|
seeds := []struct {
|
||||||
Name string
|
Name string
|
||||||
Status utils.KandangStatus
|
Status utils.KandangStatus
|
||||||
|
Capacity float64
|
||||||
Location string
|
Location string
|
||||||
PicKey string
|
PicKey string
|
||||||
}{
|
}{
|
||||||
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
|
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
|
||||||
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
|
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"},
|
||||||
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
|
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
|
||||||
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
|
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string]uint, len(seeds))
|
result := make(map[string]uint, len(seeds))
|
||||||
@@ -571,52 +572,46 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
|||||||
Flags: []utils.FlagType{utils.FlagDOC},
|
Flags: []utils.FlagType{utils.FlagDOC},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Ayam Afkir",
|
Name: "Ayam Pullet",
|
||||||
Brand: "-",
|
Brand: "MBU Pullet",
|
||||||
Sku: "1",
|
Sku: "PLT0001",
|
||||||
Uom: "Ekor",
|
Uom: "Ekor",
|
||||||
Category: "Day Old Chick",
|
Category: "Pullet",
|
||||||
Price: 1,
|
Price: 15000,
|
||||||
|
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
||||||
|
Flags: []utils.FlagType{utils.FlagPullet},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Ayam Mati",
|
Name: "Ayam Afkir",
|
||||||
Brand: "-",
|
Brand: "-",
|
||||||
Sku: "2",
|
Sku: "1",
|
||||||
Uom: "Ekor",
|
Uom: "Ekor",
|
||||||
Category: "Day Old Chick",
|
Category: "Day Old Chick",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Ayam Culling",
|
Name: "Ayam Culling",
|
||||||
Brand: "-",
|
Brand: "-",
|
||||||
Sku: "3",
|
Sku: "3",
|
||||||
Uom: "Ekor",
|
Uom: "Ekor",
|
||||||
Category: "Day Old Chick",
|
Category: "Day Old Chick",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Telur Konsumsi Baik",
|
Name: "Telur Konsumsi Baik",
|
||||||
Brand: "-",
|
Brand: "-",
|
||||||
Sku: "4",
|
Sku: "4",
|
||||||
Uom: "Unit",
|
Uom: "Unit",
|
||||||
Category: "Telur",
|
Category: "Telur",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Telur Pecah",
|
Name: "Telur Pecah",
|
||||||
Brand: "-",
|
Brand: "-",
|
||||||
Sku: "5",
|
Sku: "5",
|
||||||
Uom: "Unit",
|
Uom: "Unit",
|
||||||
Category: "Telur",
|
Category: "Telur",
|
||||||
Price: 1,
|
Price: 1,
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "281 SPECIAL STARTER",
|
Name: "281 SPECIAL STARTER",
|
||||||
|
|||||||
@@ -7,17 +7,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Kandang struct {
|
type Kandang struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
||||||
Status string `gorm:"type:varchar(50);not null"`
|
Status string `gorm:"type:varchar(50);not null"`
|
||||||
LocationId uint `gorm:"not null"`
|
LocationId uint `gorm:"not null"`
|
||||||
PicId uint `gorm:"not null"`
|
Capacity float64 `gorm:"not null"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
PicId uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||||
|
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
||||||
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayingKandangTransfer struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
KandangId uint
|
||||||
|
ProductWarehouseId uint
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
LayingTransferId uint `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
|
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayingTransfer struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
TransferNumber string `gorm:"uniqueIndex;not null"`
|
||||||
|
FromProjectFlockId uint `gorm:"not null"`
|
||||||
|
ToProjectFlockId uint `gorm:"not null"`
|
||||||
|
TransferDate time.Time `gorm:"type:date;not null"`
|
||||||
|
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
UsageQty *float64 `gorm:"type:numeric(15,3)"`
|
||||||
|
Notes string `gorm:"type:text"`
|
||||||
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
|
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
|
||||||
|
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
|
||||||
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||||
|
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||||
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayingTransferSource struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
LayingTransferId uint `gorm:"index;not null"`
|
||||||
|
SourceProjectFlockKandangId uint `gorm:"not null"`
|
||||||
|
ProductWarehouseId *uint `gorm:""`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
Note string `gorm:"type:text"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
|
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
|
||||||
|
SourceProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:SourceProjectFlockKandangId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayingTransferTarget struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
LayingTransferId uint `gorm:"index;not null"`
|
||||||
|
TargetProjectFlockKandangId uint `gorm:"not null"`
|
||||||
|
Qty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
ProductWarehouseId *uint `gorm:""`
|
||||||
|
Note string `gorm:"type:text"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
|
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
|
||||||
|
TargetProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:TargetProjectFlockKandangId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
}
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ type Location struct {
|
|||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Area Area `gorm:"foreignKey:AreaId;references:Id"`
|
Area Area `gorm:"foreignKey:AreaId;references:Id"`
|
||||||
|
Kandangs []Kandang `gorm:"foreignKey:LocationId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,16 @@ type ProjectChickin struct {
|
|||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||||
ChickInDate time.Time `gorm:"not null"`
|
ChickInDate time.Time `gorm:"not null"`
|
||||||
Quantity float64 `gorm:"not null"`
|
ProductWarehouseId uint `gorm:"not null"`
|
||||||
Note string `gorm:"type:text"`
|
UsageQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
Notes string `gorm:"type:text"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProjectFlockPopulation struct {
|
type ProjectFlockPopulation struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
ProjectChickinId uint `gorm:"not null"`
|
||||||
InitialQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
ProductWarehouseId uint `gorm:"not null"`
|
||||||
CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
TotalQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
ReservedQuantity float64 `gorm:"type:numeric(15,3)"`
|
TotalUsedQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
Notes string `gorm:"type:text"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ type ProjectFlock struct {
|
|||||||
Category string `gorm:"type:varchar(20);not null"`
|
Category string `gorm:"type:varchar(20);not null"`
|
||||||
FcrId uint `gorm:"not null"`
|
FcrId uint `gorm:"not null"`
|
||||||
LocationId uint `gorm:"not null"`
|
LocationId uint `gorm:"not null"`
|
||||||
Period int `gorm:"not null"`
|
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ package entities
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type ProjectFlockKandang struct {
|
type ProjectFlockKandang struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
|
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||||
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
|
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
Period int `gorm:"not null"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
|
||||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
|
||||||
|
|
||||||
|
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
|
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
|
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Purchase struct {
|
||||||
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
PrNumber string `gorm:"not null"`
|
||||||
|
PoNumber *string
|
||||||
|
PoDate *time.Time
|
||||||
|
SupplierId uint64 `gorm:"not null"`
|
||||||
|
CreditTerm *int
|
||||||
|
DueDate *time.Time
|
||||||
|
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
Notes *string
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
|
CreatedBy uint64 `gorm:"not null"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
|
Items []PurchaseItem `gorm:"foreignKey:PurchaseId"`
|
||||||
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseItem struct {
|
||||||
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
|
PurchaseId uint64 `gorm:"not null"`
|
||||||
|
ProductId uint64 `gorm:"not null"`
|
||||||
|
WarehouseId uint64 `gorm:"not null"`
|
||||||
|
ProductWarehouseId *uint64
|
||||||
|
ReceivedDate *time.Time
|
||||||
|
TravelNumber *string
|
||||||
|
TravelNumberDocs *string
|
||||||
|
VehicleNumber *string
|
||||||
|
SubQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
|
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
||||||
|
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
|
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||||
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ApprovalBaseDTO struct {
|
type ApprovalBaseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
StepNumber uint16 `json:"step_number"`
|
StepNumber uint16 `json:"step_number"`
|
||||||
StepName string `json:"step_name"`
|
StepName string `json:"step_name"`
|
||||||
Action *string `json:"action"`
|
Action *string `json:"action"`
|
||||||
@@ -27,6 +28,7 @@ type ApprovalGroupDTO struct {
|
|||||||
|
|
||||||
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
|
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
|
||||||
dto := ApprovalBaseDTO{
|
dto := ApprovalBaseDTO{
|
||||||
|
Id: e.Id,
|
||||||
Notes: e.Notes,
|
Notes: e.Notes,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
|
|||||||
"LOKASI",
|
"LOKASI",
|
||||||
"KANDANG",
|
"KANDANG",
|
||||||
},
|
},
|
||||||
|
"stock_log": map[string][]string{
|
||||||
|
"log_types": []string{"TRANSFER", "ADJUSTMENT"},
|
||||||
|
"transaction_types": []string{"INCREASE", "DECREASE"},
|
||||||
|
},
|
||||||
"supplier_categories": []string{
|
"supplier_categories": []string{
|
||||||
"BOP",
|
"BOP",
|
||||||
"SAPRONAK",
|
"SAPRONAK",
|
||||||
|
|||||||
+133
-7
@@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
@@ -19,8 +20,13 @@ type ProductWarehouseRepository interface {
|
|||||||
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
|
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
|
||||||
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
|
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
|
||||||
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
|
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
|
||||||
|
GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
|
||||||
|
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
|
||||||
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
|
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
|
||||||
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
|
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
|
||||||
|
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
|
||||||
|
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, createdBy uint64) (uint, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductWarehouseRepositoryImpl struct {
|
type ProductWarehouseRepositoryImpl struct {
|
||||||
@@ -78,14 +84,14 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous
|
|||||||
|
|
||||||
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
|
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
|
||||||
var productWarehouses []entity.ProductWarehouse
|
var productWarehouses []entity.ProductWarehouse
|
||||||
err := r.DB().WithContext(ctx).
|
q := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
|
||||||
Table("product_warehouses").
|
|
||||||
Select("product_warehouses.*").
|
|
||||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
||||||
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
|
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
|
||||||
Order("product_warehouses.created_at DESC").
|
Order("product_warehouses.created_at DESC")
|
||||||
Find(&productWarehouses).Error
|
|
||||||
|
// preload relations so nested Product and Warehouse are populated
|
||||||
|
err := q.Preload("Product").Preload("Warehouse").Find(&productWarehouses).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -100,12 +106,12 @@ func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(c
|
|||||||
}
|
}
|
||||||
fmt.Println(warehouseId)
|
fmt.Println(warehouseId)
|
||||||
err := query.WithContext(ctx).
|
err := query.WithContext(ctx).
|
||||||
Table("product_warehouses").
|
Model(&entity.ProductWarehouse{}).
|
||||||
Select("product_warehouses.*").
|
|
||||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
||||||
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
|
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
|
||||||
Order("product_warehouses.created_at DESC").
|
Order("product_warehouses.created_at DESC").
|
||||||
|
Preload("Product").Preload("Warehouse").
|
||||||
First(&productWarehouse).Error
|
First(&productWarehouse).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -146,3 +152,123 @@ func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, d
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error {
|
||||||
|
if len(affected) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := make([]uint, 0, len(affected))
|
||||||
|
for id := range affected {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
var emptyIDs []uint
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Model(&entity.ProductWarehouse{}).
|
||||||
|
Where("id IN ? AND COALESCE(quantity,0) <= 0", ids).
|
||||||
|
Pluck("id", &emptyIDs).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(emptyIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Model(&entity.PurchaseItem{}).
|
||||||
|
Where("product_warehouse_id IN ?", emptyIDs).
|
||||||
|
Update("product_warehouse_id", nil).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Where("id IN ?", emptyIDs).
|
||||||
|
Delete(&entity.ProductWarehouse{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
|
||||||
|
ctx context.Context,
|
||||||
|
productID uint,
|
||||||
|
warehouseID uint,
|
||||||
|
createdBy uint64,
|
||||||
|
) (uint, error) {
|
||||||
|
record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
|
||||||
|
if err == nil {
|
||||||
|
return record.Id, nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
entity := &entity.ProductWarehouse{
|
||||||
|
ProductId: productID,
|
||||||
|
WarehouseId: warehouseID,
|
||||||
|
Quantity: 0,
|
||||||
|
CreatedBy: uint(createdBy),
|
||||||
|
}
|
||||||
|
if entity.CreatedBy == 0 {
|
||||||
|
entity.CreatedBy = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.CreateOne(ctx, entity, nil); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return entity.Id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) {
|
||||||
|
var productWarehouse entity.ProductWarehouse
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Preload("Product").
|
||||||
|
Preload("Warehouse").
|
||||||
|
Preload("Warehouse.Area").
|
||||||
|
Preload("Warehouse.Location").
|
||||||
|
First(&productWarehouse, id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &productWarehouse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) {
|
||||||
|
var product entity.Product
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
||||||
|
Where("product_categories.code = ?", categoryCode).
|
||||||
|
First(&product).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &product, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error) {
|
||||||
|
var productWarehouses []entity.ProductWarehouse
|
||||||
|
err := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
|
||||||
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
|
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products").
|
||||||
|
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
|
||||||
|
Order("product_warehouses.created_at DESC").
|
||||||
|
Preload("Product").Preload("Warehouse").
|
||||||
|
Find(&productWarehouses).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return productWarehouses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error) {
|
||||||
|
var product entity.Product
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products").
|
||||||
|
Where("flags.name = ?", flagName).
|
||||||
|
First(&product).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &product, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type CustomerBaseDTO struct {
|
|||||||
AccountNumber string `json:"account_number"`
|
AccountNumber string `json:"account_number"`
|
||||||
Balance float64 `json:"balance"`
|
Balance float64 `json:"balance"`
|
||||||
|
|
||||||
Pic *userDTO.UserBaseDTO `json:"pic"`
|
Pic *userDTO.UserBaseDTO `json:"pic,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CustomerListDTO struct {
|
type CustomerListDTO struct {
|
||||||
|
|||||||
@@ -14,15 +14,21 @@ type KandangBaseDTO struct {
|
|||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Location *locationDTO.LocationBaseDTO `json:"location"`
|
Capacity float64 `json:"capacity"`
|
||||||
Pic *userDTO.UserBaseDTO `json:"pic"`
|
Location locationDTO.LocationBaseDTO `json:"location,omitempty"`
|
||||||
|
Pic userDTO.UserBaseDTO `json:"pic,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KandangListDTO struct {
|
type KandangListDTO struct {
|
||||||
KandangBaseDTO
|
Id uint `json:"id"`
|
||||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
Name string `json:"name"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Status string `json:"status"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
Capacity float64 `json:"capacity"`
|
||||||
|
Location locationDTO.LocationBaseDTO `json:"location"`
|
||||||
|
Pic userDTO.UserBaseDTO `json:"pic"`
|
||||||
|
CreatedUser userDTO.UserBaseDTO `json:"created_user"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type KandangDetailDTO struct {
|
type KandangDetailDTO struct {
|
||||||
@@ -32,39 +38,56 @@ type KandangDetailDTO struct {
|
|||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
|
func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO {
|
||||||
var location *locationDTO.LocationBaseDTO
|
var location locationDTO.LocationBaseDTO
|
||||||
if e.Location.Id != 0 {
|
if e.Location.Id != 0 {
|
||||||
mapped := locationDTO.ToLocationBaseDTO(e.Location)
|
mapped := locationDTO.ToLocationBaseDTO(e.Location)
|
||||||
location = &mapped
|
location = mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
var pic *userDTO.UserBaseDTO
|
var pic userDTO.UserBaseDTO
|
||||||
if e.Pic.Id != 0 {
|
if e.Pic.Id != 0 {
|
||||||
mapped := userDTO.ToUserBaseDTO(e.Pic)
|
mapped := userDTO.ToUserBaseDTO(e.Pic)
|
||||||
pic = &mapped
|
pic = mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
return KandangBaseDTO{
|
return KandangBaseDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
Name: e.Name,
|
Name: e.Name,
|
||||||
Status: e.Status,
|
Status: e.Status,
|
||||||
|
Capacity: e.Capacity,
|
||||||
Location: location,
|
Location: location,
|
||||||
Pic: pic,
|
Pic: pic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToKandangListDTO(e entity.Kandang) KandangListDTO {
|
func ToKandangListDTO(e entity.Kandang) KandangListDTO {
|
||||||
var createdUser *userDTO.UserBaseDTO
|
var location locationDTO.LocationBaseDTO
|
||||||
|
if e.Location.Id != 0 {
|
||||||
|
mapped := locationDTO.ToLocationBaseDTO(e.Location)
|
||||||
|
location = mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var pic userDTO.UserBaseDTO
|
||||||
|
if e.Pic.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserBaseDTO(e.Pic)
|
||||||
|
pic = mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdUser userDTO.UserBaseDTO
|
||||||
if e.CreatedUser.Id != 0 {
|
if e.CreatedUser.Id != 0 {
|
||||||
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
|
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
|
||||||
createdUser = &mapped
|
createdUser = mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
return KandangListDTO{
|
return KandangListDTO{
|
||||||
KandangBaseDTO: ToKandangBaseDTO(e),
|
Id: e.Id,
|
||||||
CreatedAt: e.CreatedAt,
|
Name: e.Name,
|
||||||
UpdatedAt: e.UpdatedAt,
|
Status: e.Status,
|
||||||
CreatedUser: createdUser,
|
Location: location,
|
||||||
|
Pic: pic,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
CreatedUser: createdUser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -115,9 +116,41 @@ func (r *KandangRepositoryImpl) UpsertProjectFlockKandang(ctx context.Context, p
|
|||||||
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
|
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
|
||||||
First(&link).Error
|
First(&link).Error
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
var project entity.ProjectFlock
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Select("id, location_id").
|
||||||
|
First(&project, projectFlockID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var kandang entity.Kandang
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Select("id, location_id").
|
||||||
|
First(&kandang, kandangID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if kandang.LocationId != project.LocationId {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Kandang tidak berada pada lokasi yang sama dengan project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine project's period from existing pivot rows so the new kandang
|
||||||
|
// shares the same period.
|
||||||
|
var period int
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Table("project_flock_kandangs").
|
||||||
|
Where("project_flock_id = ?", projectFlockID).
|
||||||
|
Select("COALESCE(MAX(period), 0)").
|
||||||
|
Scan(&period).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if period <= 0 {
|
||||||
|
period = 1
|
||||||
|
}
|
||||||
|
|
||||||
link = entity.ProjectFlockKandang{
|
link = entity.ProjectFlockKandang{
|
||||||
ProjectFlockId: projectFlockID,
|
ProjectFlockId: projectFlockID,
|
||||||
KandangId: kandangID,
|
KandangId: kandangID,
|
||||||
|
Period: period,
|
||||||
}
|
}
|
||||||
return r.db.WithContext(ctx).Create(&link).Error
|
return r.db.WithContext(ctx).Create(&link).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package kandangs
|
package kandangs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/controllers"
|
||||||
kandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
|
kandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,7 +13,7 @@ func KandangRoutes(v1 fiber.Router, u user.UserService, s kandang.KandangService
|
|||||||
ctrl := controller.NewKandangController(s)
|
ctrl := controller.NewKandangController(s)
|
||||||
|
|
||||||
route := v1.Group("/kandangs")
|
route := v1.Group("/kandangs")
|
||||||
route.Use(m.Auth(u))
|
// route.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
createBody := &entity.Kandang{
|
createBody := &entity.Kandang{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
LocationId: req.LocationId,
|
LocationId: req.LocationId,
|
||||||
|
Capacity: req.Capacity,
|
||||||
Status: status,
|
Status: status,
|
||||||
PicId: req.PicId,
|
PicId: req.PicId,
|
||||||
CreatedBy: 1,
|
CreatedBy: 1,
|
||||||
@@ -194,6 +195,10 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
updateBody["pic_id"] = *req.PicId
|
updateBody["pic_id"] = *req.PicId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Capacity != nil {
|
||||||
|
updateBody["capacity"] = *req.Capacity
|
||||||
|
}
|
||||||
|
|
||||||
finalStatus := strings.ToUpper(existing.Status)
|
finalStatus := strings.ToUpper(existing.Status)
|
||||||
if req.Status != nil {
|
if req.Status != nil {
|
||||||
status := strings.ToUpper(*req.Status)
|
status := strings.ToUpper(*req.Status)
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
Name string `json:"name" validate:"required_strict,min=3"`
|
Name string `json:"name" validate:"required_strict,min=3"`
|
||||||
Status string `json:"status,omitempty" validate:"omitempty,min=3"`
|
Status string `json:"status,omitempty" validate:"omitempty,min=3"`
|
||||||
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
|
Capacity float64 `json:"capacity" validate:"required_strict,gt=0"`
|
||||||
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
|
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
|
||||||
ProjectFlockId *uint `json:"project_flock_id" validate:"omitempty,number,gt=0"`
|
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
|
||||||
|
ProjectFlockId *uint `json:"project_flock_id" validate:"omitempty,number,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
Name *string `json:"name,omitempty" validate:"omitempty"`
|
||||||
Status *string `json:"status,omitempty" validate:"omitempty,min=3"`
|
Status *string `json:"status,omitempty" validate:"omitempty,min=3"`
|
||||||
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
|
Capacity *float64 `json:"capacity" validate:"omitempty,gt=0"`
|
||||||
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
|
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||||
ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty,number,gt=0"`
|
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||||
|
ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
|
|||||||
@@ -14,11 +14,14 @@ type LocationBaseDTO struct {
|
|||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
Area *areaDTO.AreaBaseDTO `json:"area"`
|
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocationListDTO struct {
|
type LocationListDTO struct {
|
||||||
LocationBaseDTO
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Area *areaDTO.AreaBaseDTO `json:"area"`
|
||||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -52,11 +55,20 @@ func ToLocationListDTO(e entity.Location) LocationListDTO {
|
|||||||
createdUser = &mapped
|
createdUser = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var area *areaDTO.AreaBaseDTO
|
||||||
|
if e.Area.Id != 0 {
|
||||||
|
mapped := areaDTO.ToAreaBaseDTO(e.Area)
|
||||||
|
area = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
return LocationListDTO{
|
return LocationListDTO{
|
||||||
LocationBaseDTO: ToLocationBaseDTO(e),
|
Id: e.Id,
|
||||||
CreatedUser: createdUser,
|
Name: e.Name,
|
||||||
CreatedAt: e.CreatedAt,
|
Address: e.Address,
|
||||||
UpdatedAt: e.UpdatedAt,
|
Area: area,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,16 +12,17 @@ import (
|
|||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
|
|
||||||
type NonstockBaseDTO struct {
|
type NonstockBaseDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
UomID uint `json:"uom_id"`
|
Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"`
|
||||||
|
Flags []string `json:"flags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NonstockListDTO struct {
|
type NonstockListDTO struct {
|
||||||
NonstockBaseDTO
|
Id uint `json:"id"`
|
||||||
Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"`
|
Name string `json:"name"`
|
||||||
|
Uom *uomDTO.UomBaseDTO `json:"uom"`
|
||||||
Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"`
|
Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"`
|
||||||
Flags []string `json:"flags"`
|
|
||||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -35,10 +36,22 @@ type NonstockDetailDTO struct {
|
|||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToNonstockBaseDTO(e entity.Nonstock) NonstockBaseDTO {
|
func ToNonstockBaseDTO(e entity.Nonstock) NonstockBaseDTO {
|
||||||
|
var uomRef *uomDTO.UomBaseDTO
|
||||||
|
if e.Uom.Id != 0 {
|
||||||
|
mapped := uomDTO.ToUomBaseDTO(e.Uom)
|
||||||
|
uomRef = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := make([]string, len(e.Flags))
|
||||||
|
for i, f := range e.Flags {
|
||||||
|
flags[i] = f.Name
|
||||||
|
}
|
||||||
|
|
||||||
return NonstockBaseDTO{
|
return NonstockBaseDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
Name: e.Name,
|
Name: e.Name,
|
||||||
UomID: e.UomId,
|
Uom: uomRef,
|
||||||
|
Flags: flags,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,13 +79,13 @@ func ToNonstockListDTO(e entity.Nonstock) NonstockListDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return NonstockListDTO{
|
return NonstockListDTO{
|
||||||
NonstockBaseDTO: ToNonstockBaseDTO(e),
|
Id: e.Id,
|
||||||
CreatedAt: e.CreatedAt,
|
Name: e.Name,
|
||||||
UpdatedAt: e.UpdatedAt,
|
Uom: uomRef,
|
||||||
CreatedUser: createdUser,
|
CreatedAt: e.CreatedAt,
|
||||||
Uom: uomRef,
|
UpdatedAt: e.UpdatedAt,
|
||||||
Suppliers: suppliers,
|
CreatedUser: createdUser,
|
||||||
Flags: flags,
|
Suppliers: suppliers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import (
|
|||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
|
|
||||||
type ProductBaseDTO struct {
|
type ProductBaseDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"`
|
||||||
|
Flags []string `json:"flags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductListDTO struct {
|
type ProductListDTO struct {
|
||||||
@@ -25,10 +27,8 @@ type ProductListDTO struct {
|
|||||||
SellingPrice *float64 `json:"selling_price,omitempty"`
|
SellingPrice *float64 `json:"selling_price,omitempty"`
|
||||||
Tax *float64 `json:"tax,omitempty"`
|
Tax *float64 `json:"tax,omitempty"`
|
||||||
ExpiryPeriod *int `json:"expiry_period,omitempty"`
|
ExpiryPeriod *int `json:"expiry_period,omitempty"`
|
||||||
Uom *uomDTO.UomBaseDTO `json:"uom,omitempty"`
|
|
||||||
ProductCategory *productCategoryDTO.ProductCategoryBaseDTO `json:"product_category,omitempty"`
|
ProductCategory *productCategoryDTO.ProductCategoryBaseDTO `json:"product_category,omitempty"`
|
||||||
Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"`
|
Suppliers []supplierDTO.SupplierBaseDTO `json:"suppliers"`
|
||||||
Flags []string `json:"flags"`
|
|
||||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -42,9 +42,22 @@ type ProductDetailDTO struct {
|
|||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToProductBaseDTO(e entity.Product) ProductBaseDTO {
|
func ToProductBaseDTO(e entity.Product) ProductBaseDTO {
|
||||||
|
flags := make([]string, len(e.Flags))
|
||||||
|
for i, f := range e.Flags {
|
||||||
|
flags[i] = f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
var uomRef *uomDTO.UomBaseDTO
|
||||||
|
if e.Uom.Id != 0 {
|
||||||
|
mapped := uomDTO.ToUomBaseDTO(e.Uom)
|
||||||
|
uomRef = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
return ProductBaseDTO{
|
return ProductBaseDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
Name: e.Name,
|
Name: e.Name,
|
||||||
|
Flags: flags,
|
||||||
|
Uom: uomRef,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,12 +68,6 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
|
|||||||
createdUser = &mapped
|
createdUser = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
var uomRef *uomDTO.UomBaseDTO
|
|
||||||
if e.Uom.Id != 0 {
|
|
||||||
mapped := uomDTO.ToUomBaseDTO(e.Uom)
|
|
||||||
uomRef = &mapped
|
|
||||||
}
|
|
||||||
|
|
||||||
var categoryRef *productCategoryDTO.ProductCategoryBaseDTO
|
var categoryRef *productCategoryDTO.ProductCategoryBaseDTO
|
||||||
if e.ProductCategory.Id != 0 {
|
if e.ProductCategory.Id != 0 {
|
||||||
mapped := productCategoryDTO.ToProductCategoryBaseDTO(e.ProductCategory)
|
mapped := productCategoryDTO.ToProductCategoryBaseDTO(e.ProductCategory)
|
||||||
@@ -72,11 +79,6 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
|
|||||||
suppliers[i] = supplierDTO.ToSupplierBaseDTO(s)
|
suppliers[i] = supplierDTO.ToSupplierBaseDTO(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
flags := make([]string, len(e.Flags))
|
|
||||||
for i, f := range e.Flags {
|
|
||||||
flags[i] = f.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProductListDTO{
|
return ProductListDTO{
|
||||||
Brand: e.Brand,
|
Brand: e.Brand,
|
||||||
Sku: e.Sku,
|
Sku: e.Sku,
|
||||||
@@ -88,10 +90,8 @@ func ToProductListDTO(e entity.Product) ProductListDTO {
|
|||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
Uom: uomRef,
|
|
||||||
ProductCategory: categoryRef,
|
ProductCategory: categoryRef,
|
||||||
Suppliers: suppliers,
|
Suppliers: suppliers,
|
||||||
Flags: flags,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type ProductRepository interface {
|
|||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
CategoryExists(ctx context.Context, categoryID uint) (bool, error)
|
CategoryExists(ctx context.Context, categoryID uint) (bool, error)
|
||||||
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
|
GetSuppliersByIDs(ctx context.Context, supplierIDs []uint) ([]entity.Supplier, error)
|
||||||
|
IsLinkedToSupplier(ctx context.Context, productID, supplierID uint) (bool, error)
|
||||||
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error
|
SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error
|
||||||
SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error
|
SyncFlags(ctx context.Context, tx *gorm.DB, productID uint, flags []string) error
|
||||||
DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error
|
DeleteFlags(ctx context.Context, tx *gorm.DB, productID uint) error
|
||||||
@@ -90,6 +91,17 @@ func (r *ProductRepositoryImpl) GetSuppliersByIDs(ctx context.Context, supplierI
|
|||||||
return suppliers, nil
|
return suppliers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ProductRepositoryImpl) IsLinkedToSupplier(ctx context.Context, productID, supplierID uint) (bool, error) {
|
||||||
|
var count int64
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Model(&entity.ProductSupplier{}).
|
||||||
|
Where("product_id = ? AND supplier_id = ?", productID, supplierID).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error {
|
func (r *ProductRepositoryImpl) SyncSuppliersDiff(ctx context.Context, tx *gorm.DB, productID uint, supplierIDs []uint) error {
|
||||||
db := tx
|
db := tx
|
||||||
if db == nil {
|
if db == nil {
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ type UomBaseDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UomListDTO struct {
|
type UomListDTO struct {
|
||||||
UomBaseDTO
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -42,7 +43,8 @@ func ToUomListDTO(e entity.Uom) UomListDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return UomListDTO{
|
return UomListDTO{
|
||||||
UomBaseDTO: ToUomBaseDTO(e),
|
Id: e.Id,
|
||||||
|
Name: e.Name,
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
|
|||||||
@@ -16,16 +16,21 @@ type WarehouseBaseDTO struct {
|
|||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Area *areaDTO.AreaBaseDTO `json:"area"`
|
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
|
||||||
Location *locationDTO.LocationBaseDTO `json:"location"`
|
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
|
||||||
Kandang *kandangDTO.KandangBaseDTO `json:"kandang"`
|
Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WarehouseListDTO struct {
|
type WarehouseListDTO struct {
|
||||||
WarehouseBaseDTO
|
Id uint `json:"id"`
|
||||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
Name string `json:"name"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Type string `json:"type"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
Area *areaDTO.AreaBaseDTO `json:"area"`
|
||||||
|
Location *locationDTO.LocationBaseDTO `json:"location"`
|
||||||
|
Kandang *kandangDTO.KandangBaseDTO `json:"kandang"`
|
||||||
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WarehouseDetailDTO struct {
|
type WarehouseDetailDTO struct {
|
||||||
@@ -70,11 +75,34 @@ func ToWarehouseListDTO(e entity.Warehouse) WarehouseListDTO {
|
|||||||
createdUser = &mapped
|
createdUser = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var area *areaDTO.AreaBaseDTO
|
||||||
|
if e.Area.Id != 0 {
|
||||||
|
mapped := areaDTO.ToAreaBaseDTO(e.Area)
|
||||||
|
area = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var location *locationDTO.LocationBaseDTO
|
||||||
|
if e.Location != nil && e.Location.Id != 0 {
|
||||||
|
mapped := locationDTO.ToLocationBaseDTO(*e.Location)
|
||||||
|
location = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
var kandang *kandangDTO.KandangBaseDTO
|
||||||
|
if e.Kandang != nil && e.Kandang.Id != 0 {
|
||||||
|
mapped := kandangDTO.ToKandangBaseDTO(*e.Kandang)
|
||||||
|
kandang = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
return WarehouseListDTO{
|
return WarehouseListDTO{
|
||||||
WarehouseBaseDTO: ToWarehouseBaseDTO(e),
|
Id: e.Id,
|
||||||
CreatedAt: e.CreatedAt,
|
Name: e.Name,
|
||||||
UpdatedAt: e.UpdatedAt,
|
Type: e.Type,
|
||||||
CreatedUser: createdUser,
|
Area: area,
|
||||||
|
Location: location,
|
||||||
|
Kandang: kandang,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
CreatedUser: createdUser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ type WarehouseRepository interface {
|
|||||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
|
GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
|
||||||
|
GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
|
||||||
|
GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type WarehouseRepositoryImpl struct {
|
type WarehouseRepositoryImpl struct {
|
||||||
@@ -60,3 +62,28 @@ func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId
|
|||||||
}
|
}
|
||||||
return &warehouse, nil
|
return &warehouse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *WarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error) {
|
||||||
|
var warehouse entity.Warehouse
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Area").
|
||||||
|
Preload("Location").
|
||||||
|
First(&warehouse, id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &warehouse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *WarehouseRepositoryImpl) GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) {
|
||||||
|
var warehouse entity.Warehouse
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("kandang_id = ?", kandangId).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Order("id DESC").
|
||||||
|
First(&warehouse).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &warehouse, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto"
|
||||||
@@ -22,30 +21,84 @@ func NewChickinController(chickinService service.ChickinService) *ChickinControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ChickinController) GetAll(c *fiber.Ctx) error {
|
// func (u *ChickinController) GetAll(c *fiber.Ctx) error {
|
||||||
query := &validation.Query{
|
// query := &validation.Query{
|
||||||
Page: c.QueryInt("page", 1),
|
// Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
// Limit: c.QueryInt("limit", 10),
|
||||||
ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
|
// ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)),
|
||||||
|
// }
|
||||||
|
|
||||||
|
// result, totalResults, err := u.ChickinService.GetAll(c, query)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return c.Status(fiber.StatusOK).
|
||||||
|
// JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{
|
||||||
|
// Code: fiber.StatusOK,
|
||||||
|
// Status: "success",
|
||||||
|
// Message: "Get all chickins successfully",
|
||||||
|
// Meta: response.Meta{
|
||||||
|
// Page: query.Page,
|
||||||
|
// Limit: query.Limit,
|
||||||
|
// TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
// TotalResults: totalResults,
|
||||||
|
// },
|
||||||
|
// Data: dto.ToChickinListDTOs(result),
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (u *ChickinController) GetOne(c *fiber.Ctx) error {
|
||||||
|
// param := c.Params("id")
|
||||||
|
|
||||||
|
// id, err := strconv.Atoi(param)
|
||||||
|
// if err != nil {
|
||||||
|
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// result, err := u.ChickinService.GetOne(c, uint(id))
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return c.Status(fiber.StatusOK).
|
||||||
|
// JSON(response.Success{
|
||||||
|
// Code: fiber.StatusOK,
|
||||||
|
// Status: "success",
|
||||||
|
// Message: "Get chickin successfully",
|
||||||
|
// Data: dto.ToChickinListDTO(*result),
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (u *ChickinController) CreateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Create)
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.ChickinService.GetAll(c, query)
|
results, err := u.ChickinService.CreateOne(c, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
var (
|
||||||
JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{
|
data interface{}
|
||||||
Code: fiber.StatusOK,
|
message = "Create chickin successfully"
|
||||||
|
)
|
||||||
|
if len(results) == 1 {
|
||||||
|
data = dto.ToChickinListDTO(results[0])
|
||||||
|
} else {
|
||||||
|
message = "Create chickins successfully"
|
||||||
|
data = dto.ToChickinListDTOs(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusCreated,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get all chickins successfully",
|
Message: message,
|
||||||
Meta: response.Meta{
|
Data: data,
|
||||||
Page: query.Page,
|
|
||||||
Limit: query.Limit,
|
|
||||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
|
||||||
TotalResults: totalResults,
|
|
||||||
},
|
|
||||||
Data: dto.ToChickinListDTOs(result),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,95 +120,85 @@ func (u *ChickinController) GetOne(c *fiber.Ctx) error {
|
|||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get chickin successfully",
|
Message: "Get chickin successfully",
|
||||||
Data: dto.ToChickinListDTO(*result),
|
Data: dto.ToChickinDetailDTO(*result),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ChickinController) CreateOne(c *fiber.Ctx) error {
|
// func (u *ChickinController) UpdateOne(c *fiber.Ctx) error {
|
||||||
req := new(validation.Create)
|
// req := new(validation.Update)
|
||||||
|
// param := c.Params("id")
|
||||||
|
|
||||||
|
// id, err := strconv.Atoi(param)
|
||||||
|
// if err != nil {
|
||||||
|
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if err := c.BodyParser(req); err != nil {
|
||||||
|
// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// result, err := u.ChickinService.UpdateOne(c, req, uint(id))
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return c.Status(fiber.StatusOK).
|
||||||
|
// JSON(response.Success{
|
||||||
|
// Code: fiber.StatusOK,
|
||||||
|
// Status: "success",
|
||||||
|
// Message: "Update chickin successfully",
|
||||||
|
// Data: dto.ToChickinListDTO(*result),
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
|
||||||
|
// param := c.Params("id")
|
||||||
|
|
||||||
|
// id, err := strconv.Atoi(param)
|
||||||
|
// if err != nil {
|
||||||
|
// return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return c.Status(fiber.StatusOK).
|
||||||
|
// JSON(response.Common{
|
||||||
|
// Code: fiber.StatusOK,
|
||||||
|
// Status: "success",
|
||||||
|
// Message: "Delete chickin successfully",
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (u *ChickinController) Approval(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Approve)
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
if err := c.BodyParser(req); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := u.ChickinService.CreateOne(c, req)
|
results, err := u.ChickinService.Approval(c, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).
|
var (
|
||||||
JSON(response.Success{
|
data interface{}
|
||||||
Code: fiber.StatusCreated,
|
message = "Submit chickin approval successfully"
|
||||||
Status: "success",
|
)
|
||||||
Message: "Create chickin successfully",
|
if len(results) == 1 {
|
||||||
Data: dto.ToChickinListDTO(*result),
|
data = dto.ToChickinListDTO(results[0])
|
||||||
})
|
} else {
|
||||||
}
|
message = "Submit chickin approvals successfully"
|
||||||
|
data = dto.ToChickinListDTOs(results)
|
||||||
func (u *ChickinController) UpdateOne(c *fiber.Ctx) error {
|
|
||||||
req := new(validation.Update)
|
|
||||||
param := c.Params("id")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.ChickinService.UpdateOne(c, req, uint(id))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.Success{
|
JSON(response.Success{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Update chickin successfully",
|
Message: message,
|
||||||
Data: dto.ToChickinListDTO(*result),
|
Data: data,
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("id")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := u.ChickinService.DeleteOne(c, uint(id)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Common{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Delete chickin successfully",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ChickinController) Approve(c *fiber.Ctx) error {
|
|
||||||
param := c.Params("id")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := u.ChickinService.Approve(c, uint(id)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Approve chickin successfully",
|
|
||||||
Data: nil,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ import (
|
|||||||
// === DTO Structs (ordered) ===
|
// === DTO Structs (ordered) ===
|
||||||
|
|
||||||
type ChickinBaseDTO struct {
|
type ChickinBaseDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||||
ChickInDate time.Time `json:"chick_in_date"`
|
ChickInDate time.Time `json:"chick_in_date"`
|
||||||
Quantity float64 `json:"quantity"`
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
Note string `json:"note"`
|
UsageQty float64 `json:"usage_qty"`
|
||||||
|
PendingUsageQty float64 `json:"pending_usage_qty"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectFlockDTO struct {
|
type ProjectFlockDTO struct {
|
||||||
@@ -45,21 +47,32 @@ type ChickinSimpleDTO struct {
|
|||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||||
ChickInDate time.Time `json:"chick_in_date"`
|
ChickInDate time.Time `json:"chick_in_date"`
|
||||||
Quantity float64 `json:"quantity"`
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
Note string `json:"note"`
|
UsageQty float64 `json:"usage_qty"`
|
||||||
|
PendingUsageQty float64 `json:"pending_usage_qty"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
CreatedBy uint `json:"created_by"`
|
CreatedBy uint `json:"created_by"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChickinListDTO struct {
|
type ChickinListDTO struct {
|
||||||
ChickinBaseDTO
|
ChickinBaseDTO
|
||||||
ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"`
|
CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"`
|
||||||
CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChickinDetailDTO struct {
|
type ChickinDetailDTO struct {
|
||||||
ChickinListDTO
|
Id uint `json:"id"`
|
||||||
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||||
|
ChickInDate time.Time `json:"chick_in_date"`
|
||||||
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
|
UsageQty float64 `json:"usage_qty"`
|
||||||
|
PendingUsageQty float64 `json:"pending_usage_qty"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
CreatedBy uint `json:"created_by"`
|
||||||
|
CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Mapper Functions (ordered) ===
|
// === Mapper Functions (ordered) ===
|
||||||
@@ -87,7 +100,8 @@ func ToUserBaseDTO(e entity.User) userBaseDTO.UserBaseDTO {
|
|||||||
return userBaseDTO.ToUserBaseDTO(e)
|
return userBaseDTO.ToUserBaseDTO(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO {
|
func ToProjectFlockDTO(pfk entity.ProjectFlockKandang) ProjectFlockDTO {
|
||||||
|
e := pfk.ProjectFlock
|
||||||
var flock *flockBaseDTO.FlockBaseDTO
|
var flock *flockBaseDTO.FlockBaseDTO
|
||||||
if base := pfutils.DeriveBaseName(e.FlockName); base != "" {
|
if base := pfutils.DeriveBaseName(e.FlockName); base != "" {
|
||||||
summary := flockBaseDTO.FlockBaseDTO{Id: 0, Name: base}
|
summary := flockBaseDTO.FlockBaseDTO{Id: 0, Name: base}
|
||||||
@@ -110,7 +124,7 @@ func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO {
|
|||||||
}
|
}
|
||||||
return ProjectFlockDTO{
|
return ProjectFlockDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
Period: e.Period,
|
Period: pfk.Period,
|
||||||
Category: e.Category,
|
Category: e.Category,
|
||||||
Flock: flock,
|
Flock: flock,
|
||||||
Area: area,
|
Area: area,
|
||||||
@@ -122,7 +136,7 @@ func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO {
|
|||||||
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
|
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
|
||||||
var pf *ProjectFlockDTO
|
var pf *ProjectFlockDTO
|
||||||
if e.ProjectFlock.Id != 0 {
|
if e.ProjectFlock.Id != 0 {
|
||||||
mapped := ToProjectFlockDTO(e.ProjectFlock)
|
mapped := ToProjectFlockDTO(e)
|
||||||
pf = &mapped
|
pf = &mapped
|
||||||
}
|
}
|
||||||
var kandang *kandangBaseDTO.KandangBaseDTO
|
var kandang *kandangBaseDTO.KandangBaseDTO
|
||||||
@@ -138,17 +152,22 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO {
|
func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO {
|
||||||
var pfk *ProjectFlockKandangDTO
|
var projectFlockKandangId uint
|
||||||
if e.ProjectFlockKandang.Id != 0 {
|
// Check if ProjectFlockKandang relation is loaded
|
||||||
mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang)
|
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 {
|
||||||
pfk = &mapped
|
projectFlockKandangId = e.ProjectFlockKandang.Id
|
||||||
|
} else if e.ProjectFlockKandangId != 0 {
|
||||||
|
// If relation is not loaded but ID is available, use the ID
|
||||||
|
projectFlockKandangId = e.ProjectFlockKandangId
|
||||||
}
|
}
|
||||||
return ChickinBaseDTO{
|
return ChickinBaseDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
ProjectFlockKandang: pfk,
|
ProjectFlockKandangId: projectFlockKandangId,
|
||||||
ChickInDate: e.ChickInDate,
|
ChickInDate: e.ChickInDate,
|
||||||
Quantity: e.Quantity,
|
ProductWarehouseId: e.ProductWarehouseId,
|
||||||
Note: e.Note,
|
UsageQty: e.UsageQty,
|
||||||
|
PendingUsageQty: e.PendingUsageQty,
|
||||||
|
Notes: e.Notes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,29 +176,25 @@ func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO {
|
|||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
||||||
ChickInDate: e.ChickInDate,
|
ChickInDate: e.ChickInDate,
|
||||||
Quantity: e.Quantity,
|
ProductWarehouseId: e.ProductWarehouseId,
|
||||||
Note: e.Note,
|
UsageQty: e.UsageQty,
|
||||||
|
PendingUsageQty: e.PendingUsageQty,
|
||||||
|
Notes: e.Notes,
|
||||||
CreatedBy: e.CreatedBy,
|
CreatedBy: e.CreatedBy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO {
|
func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO {
|
||||||
var createdUser *userBaseDTO.UserBaseDTO
|
var createdUser *userBaseDTO.UserBaseDTO
|
||||||
if e.CreatedUser.Id != 0 {
|
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
|
||||||
mapped := userBaseDTO.ToUserBaseDTO(e.CreatedUser)
|
mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser)
|
||||||
createdUser = &mapped
|
createdUser = &mapped
|
||||||
}
|
}
|
||||||
var pfk *ProjectFlockKandangDTO
|
|
||||||
if e.ProjectFlockKandang.Id != 0 {
|
|
||||||
mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang)
|
|
||||||
pfk = &mapped
|
|
||||||
}
|
|
||||||
return ChickinListDTO{
|
return ChickinListDTO{
|
||||||
ChickinBaseDTO: ToChickinBaseDTO(e),
|
ChickinBaseDTO: ToChickinBaseDTO(e),
|
||||||
ProjectFlockKandang: pfk,
|
CreatedUser: createdUser,
|
||||||
CreatedUser: createdUser,
|
CreatedAt: e.CreatedAt,
|
||||||
CreatedAt: e.CreatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +215,31 @@ func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ToChickinDetailDTO(e entity.ProjectChickin) ChickinDetailDTO {
|
func ToChickinDetailDTO(e entity.ProjectChickin) ChickinDetailDTO {
|
||||||
|
var createdUser *userBaseDTO.UserBaseDTO
|
||||||
|
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
|
||||||
|
mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser)
|
||||||
|
createdUser = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
return ChickinDetailDTO{
|
return ChickinDetailDTO{
|
||||||
ChickinListDTO: ToChickinListDTO(e),
|
Id: e.Id,
|
||||||
|
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
||||||
|
ChickInDate: e.ChickInDate,
|
||||||
|
ProductWarehouseId: e.ProductWarehouseId,
|
||||||
|
UsageQty: e.UsageQty,
|
||||||
|
PendingUsageQty: e.PendingUsageQty,
|
||||||
|
Notes: e.Notes,
|
||||||
|
CreatedBy: e.CreatedBy,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ToChickinDetailDTOs(e []entity.ProjectChickin) []ChickinDetailDTO {
|
||||||
|
result := make([]ChickinDetailDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToChickinDetailDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package chickins
|
package chickins
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
@@ -15,6 +20,8 @@ import (
|
|||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChickinModule struct{}
|
type ChickinModule struct{}
|
||||||
@@ -32,6 +39,12 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
|
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
|
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
|||||||
@@ -11,21 +11,26 @@ import (
|
|||||||
type ProjectChickinRepository interface {
|
type ProjectChickinRepository interface {
|
||||||
repository.BaseRepository[entity.ProjectChickin]
|
repository.BaseRepository[entity.ProjectChickin]
|
||||||
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error)
|
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error)
|
||||||
|
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
||||||
|
GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
||||||
|
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChickinRepositoryImpl struct {
|
type ChickinRepositoryImpl struct {
|
||||||
*repository.BaseRepositoryImpl[entity.ProjectChickin]
|
*repository.BaseRepositoryImpl[entity.ProjectChickin]
|
||||||
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChickinRepository(db *gorm.DB) ProjectChickinRepository {
|
func NewChickinRepository(db *gorm.DB) ProjectChickinRepository {
|
||||||
return &ChickinRepositoryImpl{
|
return &ChickinRepositoryImpl{
|
||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickin](db),
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickin](db),
|
||||||
|
db: db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) {
|
func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) {
|
||||||
var chickin entity.ProjectChickin
|
var chickin entity.ProjectChickin
|
||||||
err := r.DB().WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Where("project_floc_id = ?", projectFlockID).
|
Where("project_floc_id = ?", projectFlockID).
|
||||||
Where("deleted_at IS NULL").
|
Where("deleted_at IS NULL").
|
||||||
First(&chickin).Error
|
First(&chickin).Error
|
||||||
@@ -34,3 +39,43 @@ func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, pr
|
|||||||
}
|
}
|
||||||
return &chickin, nil
|
return &chickin, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
|
||||||
|
var chickins []entity.ProjectChickin
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&chickins).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return chickins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChickinRepositoryImpl) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
|
||||||
|
var chickins []entity.ProjectChickin
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Where("usage_qty = 0").
|
||||||
|
Where("pending_usage_qty > 0").
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&chickins).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return chickins, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChickinRepositoryImpl) GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
|
||||||
|
var total float64
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Model(&entity.ProjectChickin{}).
|
||||||
|
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Where("pending_usage_qty > 0").
|
||||||
|
Select("COALESCE(SUM(pending_usage_qty), 0)").
|
||||||
|
Row().Scan(&total)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|||||||
+25
@@ -1,6 +1,8 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -8,6 +10,10 @@ import (
|
|||||||
|
|
||||||
type ProjectChickinDetailRepository interface {
|
type ProjectChickinDetailRepository interface {
|
||||||
repository.BaseRepository[entity.ProjectChickinDetail]
|
repository.BaseRepository[entity.ProjectChickinDetail]
|
||||||
|
CreateOne(ctx context.Context, entity *entity.ProjectChickinDetail, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error)
|
||||||
|
WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChickinDetailRepositoryImpl struct {
|
type ChickinDetailRepositoryImpl struct {
|
||||||
@@ -19,3 +25,22 @@ func NewChickinDetailRepository(db *gorm.DB) ProjectChickinDetailRepository {
|
|||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](db),
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ChickinDetailRepositoryImpl) WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository {
|
||||||
|
return &ChickinDetailRepositoryImpl{BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](tx)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChickinDetailRepositoryImpl) DB() *gorm.DB {
|
||||||
|
return r.BaseRepositoryImpl.DB()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ChickinDetailRepositoryImpl) GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error) {
|
||||||
|
var records []entity.ProjectChickinDetail
|
||||||
|
if err := r.DB().WithContext(ctx).Where("project_chickin_id = ?", projectChickinID).Find(&records).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(records) == 0 {
|
||||||
|
return nil, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService
|
|||||||
route := v1.Group("/chickins")
|
route := v1.Group("/chickins")
|
||||||
route.Use(m.Auth(u))
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
// route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
route.Get("/:id", ctrl.GetOne)
|
route.Get("/:id", ctrl.GetOne)
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
// route.Patch("/:id", ctrl.UpdateOne)
|
||||||
route.Delete("/:id", ctrl.DeleteOne)
|
// route.Delete("/:id", ctrl.DeleteOne)
|
||||||
route.Post("/:id/approve", ctrl.Approve)
|
route.Post("/approvals", ctrl.Approval)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
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"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
@@ -21,10 +25,10 @@ import (
|
|||||||
type ChickinService interface {
|
type ChickinService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error)
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error)
|
||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
Approve(ctx *fiber.Ctx, id uint) error
|
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type chickinService struct {
|
type chickinService struct {
|
||||||
@@ -76,6 +80,7 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
|||||||
}
|
}
|
||||||
|
|
||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
chickins, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
chickins, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
if params.ProjectFlockKandangId != 0 {
|
if params.ProjectFlockKandangId != 0 {
|
||||||
@@ -103,112 +108,164 @@ func (s chickinService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectChickin, e
|
|||||||
return chickin, nil
|
return chickin, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) {
|
func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
|
projectFlockKandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get projectflock kandang: %+v", err)
|
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectflockkandang.KandangId)
|
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.KandangId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get warehouse: %+v", err)
|
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found")
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// move complex DB query into repository for cleaner service
|
var productWarehouses []entity.ProductWarehouse
|
||||||
productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id)
|
category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to get product warehouses: %+v", err)
|
var productCategoryCode string
|
||||||
return nil, err
|
switch category {
|
||||||
}
|
case string(utils.ProjectFlockCategoryGrowing):
|
||||||
if len(productWarehouses) == 0 {
|
productCategoryCode = "DOC"
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse")
|
case string(utils.ProjectFlockCategoryLaying):
|
||||||
}
|
productCategoryCode = "PULLET"
|
||||||
totalQuantity := 0.0
|
default:
|
||||||
for _, pw := range productWarehouses {
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Unknown category: %s", category))
|
||||||
totalQuantity += pw.Quantity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if totalQuantity < 1 {
|
productWarehouses, err = s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id)
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses")
|
if err != nil || len(productWarehouses) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product for %s category in the Kandang's warehouse not found", strings.ToLower(category)))
|
||||||
}
|
}
|
||||||
|
|
||||||
chickinDate, err := utils.ParseDateString(req.ChickInDate)
|
chickinDate, err := utils.ParseDateString(req.ChickInDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to parse chickin date: %+v", err)
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format")
|
||||||
}
|
}
|
||||||
newChickin := &entity.ProjectChickin{
|
|
||||||
ProjectFlockKandangId: projectflockkandang.Id,
|
|
||||||
ChickInDate: chickinDate,
|
|
||||||
Quantity: totalQuantity,
|
|
||||||
Note: req.Note,
|
|
||||||
CreatedBy: 1, //todo: ganti dengan user login
|
|
||||||
}
|
|
||||||
err = s.Repository.CreateOne(c.Context(), newChickin, nil)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to create chickin: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update semua product warehouse: set quantity jadi 0
|
actorID := uint(1) // todo nanti ambil dari auth context
|
||||||
for _, pw := range productWarehouses {
|
newChikins := make([]*entity.ProjectChickin, 0)
|
||||||
err = s.ProductWarehouseRepo.PatchOne(c.Context(), pw.Id, map[string]any{
|
for _, productWarehouse := range productWarehouses {
|
||||||
"quantity": 0,
|
availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, &productWarehouse, category)
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to calculate available quantity for product warehouse %d", productWarehouse.Id))
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newChickinDetail := &entity.ProjectChickinDetail{
|
if availableQty <= 0 {
|
||||||
ProjectChickinId: newChickin.Id,
|
continue
|
||||||
ProductWarehouseId: pw.Id,
|
|
||||||
Quantity: pw.Quantity,
|
|
||||||
CreatedBy: 1, // todo: ganti dengan user login
|
|
||||||
}
|
}
|
||||||
err = s.ProjectChickinDetailRepo.CreateOne(c.Context(), newChickinDetail, nil)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to create chickin detail: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
existingPopulation, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
|
newChickin := &entity.ProjectChickin{
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
s.Log.Errorf("Failed to get project flock population: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if existingPopulation != nil {
|
|
||||||
|
|
||||||
err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), existingPopulation.Id, map[string]any{
|
|
||||||
"reserved_quantity": newChickin.Quantity + existingPopulation.ReservedQuantity,
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to update project flock population: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newPopulation := &entity.ProjectFlockPopulation{
|
|
||||||
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
||||||
InitialQuantity: 0,
|
ChickInDate: chickinDate,
|
||||||
CurrentQuantity: 0,
|
UsageQty: 0,
|
||||||
ReservedQuantity: newChickin.Quantity,
|
PendingUsageQty: availableQty,
|
||||||
CreatedBy: 1, // todo: ganti dengan user login
|
ProductWarehouseId: productWarehouse.Id,
|
||||||
}
|
Notes: req.Note,
|
||||||
err = s.ProjectflockPopulationRepo.CreateOne(c.Context(), newPopulation, nil)
|
CreatedBy: actorID,
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to create project flock population: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newChikins = append(newChikins, newChickin)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.GetOne(c, newChickin.Id)
|
if len(newChikins) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "No chickins to create")
|
||||||
|
}
|
||||||
|
|
||||||
|
existingChikins, err := s.Repository.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing chickins")
|
||||||
|
}
|
||||||
|
|
||||||
|
isFirstTime := len(existingChikins) == 0
|
||||||
|
|
||||||
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||||
|
|
||||||
|
if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
|
||||||
|
}
|
||||||
|
|
||||||
|
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
if category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
for _, chickin := range newChikins {
|
||||||
|
updates := map[string]any{"quantity": gorm.Expr("quantity - ?", chickin.PendingUsageQty)}
|
||||||
|
|
||||||
|
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickin.ProductWarehouseId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update product warehouse quantity")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var approvalAction entity.ApprovalAction
|
||||||
|
if isFirstTime {
|
||||||
|
approvalAction = entity.ApprovalActionCreated
|
||||||
|
} else {
|
||||||
|
approvalAction = entity.ApprovalActionUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
if latest == nil {
|
||||||
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlockKandang,
|
||||||
|
projectFlockKandang.Id,
|
||||||
|
utils.ProjectFlockKandangStepPengajuan,
|
||||||
|
&approvalAction,
|
||||||
|
actorID,
|
||||||
|
nil); err != nil {
|
||||||
|
if !errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if latest.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
|
||||||
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlockKandang,
|
||||||
|
projectFlockKandang.Id,
|
||||||
|
utils.ProjectFlockKandangStepPengajuan,
|
||||||
|
&approvalAction,
|
||||||
|
actorID,
|
||||||
|
nil); err != nil {
|
||||||
|
if !errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]entity.ProjectChickin, 0, len(newChikins))
|
||||||
|
for _, chickin := range newChikins {
|
||||||
|
loaded, err := s.GetOne(c, chickin.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reload chickin %d with relations: %v", chickin.Id, err))
|
||||||
|
}
|
||||||
|
result = append(result, *loaded)
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load created chickins")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) {
|
func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) {
|
||||||
@@ -222,7 +279,7 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
updateBody["chick_in_date"] = req.ChickInDate
|
updateBody["chick_in_date"] = req.ChickInDate
|
||||||
}
|
}
|
||||||
if req.Note != "" {
|
if req.Note != "" {
|
||||||
updateBody["note"] = req.Note
|
updateBody["notes"] = req.Note
|
||||||
}
|
}
|
||||||
if len(updateBody) == 0 {
|
if len(updateBody) == 0 {
|
||||||
return s.GetOne(c, id)
|
return s.GetOne(c, id)
|
||||||
@@ -240,174 +297,335 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
db := s.Repository.DB()
|
|
||||||
|
|
||||||
tx := db.WithContext(c.Context()).Begin()
|
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||||
if tx.Error != nil {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
s.Log.Errorf("Failed to begin transaction: %+v", tx.Error)
|
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
||||||
return tx.Error
|
|
||||||
}
|
|
||||||
rollback := func(err error) error {
|
|
||||||
if rerr := tx.Rollback().Error; rerr != nil {
|
|
||||||
s.Log.Errorf("Rollback failed: %+v", rerr)
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
chickinRepoTx := s.Repository.WithTx(tx)
|
return nil
|
||||||
pfkRepoTx := s.ProjectflockKandangRepo.WithTx(tx)
|
}
|
||||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(tx)
|
|
||||||
|
|
||||||
chickin, err := chickinRepoTx.GetByID(c.Context(), id, nil)
|
func (s chickinService) calculateAvailableQuantity(ctx *fiber.Ctx, projectFlockKandangID uint, productWarehouse *entity.ProductWarehouse, category string) (float64, error) {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
availableQty := productWarehouse.Quantity
|
||||||
return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found"))
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed get chickin by id: %+v", err)
|
|
||||||
return rollback(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var population entity.ProjectFlockPopulation
|
if category == string(utils.ProjectFlockCategoryGrowing) {
|
||||||
if err := tx.WithContext(c.Context()).Where("project_flock_kandang_id = ?", chickin.ProjectFlockKandangId).First(&population).Error; err != nil {
|
var totalPendingQty float64
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return rollback(fiber.NewError(fiber.StatusNotFound, "Project flock population not found"))
|
|
||||||
}
|
|
||||||
s.Log.Errorf("Failed to get project flock population: %+v", err)
|
|
||||||
return rollback(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
newReserved := population.ReservedQuantity - chickin.Quantity
|
chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
|
||||||
if newReserved < 0 {
|
if err == nil {
|
||||||
newReserved = 0
|
for _, chickin := range chickins {
|
||||||
}
|
|
||||||
if err := tx.WithContext(c.Context()).Model(&entity.ProjectFlockPopulation{}).Where("id = ?", population.Id).Updates(map[string]any{"reserved_quantity": newReserved}).Error; err != nil {
|
|
||||||
s.Log.Errorf("Failed to update project flock population: %+v", err)
|
|
||||||
return rollback(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
restoreFromDetails := func() (bool, error) {
|
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
|
||||||
var details []entity.ProjectChickinDetail
|
totalPendingQty += chickin.PendingUsageQty
|
||||||
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil {
|
}
|
||||||
return false, err
|
}
|
||||||
}
|
|
||||||
if len(details) == 0 {
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range details {
|
availableQty = productWarehouse.Quantity - totalPendingQty
|
||||||
var pw entity.ProductWarehouse
|
if availableQty < 0 {
|
||||||
if err := tx.WithContext(c.Context()).Where("id = ?", d.ProductWarehouseId).First(&pw).Error; err != nil {
|
availableQty = 0
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
}
|
||||||
|
} else if category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
var totalPopulation float64
|
||||||
|
var totalPendingQty float64
|
||||||
|
|
||||||
|
populations, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(ctx.Context(), projectFlockKandangID, productWarehouse.Id)
|
||||||
|
if err == nil {
|
||||||
|
for _, pop := range populations {
|
||||||
|
totalPopulation += pop.TotalQty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
|
||||||
|
if err == nil {
|
||||||
|
for _, chickin := range chickins {
|
||||||
|
|
||||||
|
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
|
||||||
|
totalPendingQty += chickin.PendingUsageQty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty
|
||||||
|
if availableQty < 0 {
|
||||||
|
availableQty = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableQty, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB()))
|
||||||
|
|
||||||
|
var action entity.ApprovalAction
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
|
||||||
|
case string(entity.ApprovalActionRejected):
|
||||||
|
action = entity.ApprovalActionRejected
|
||||||
|
case string(entity.ApprovalActionApproved):
|
||||||
|
action = entity.ApprovalActionApproved
|
||||||
|
default:
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
|
||||||
|
}
|
||||||
|
|
||||||
|
approvableIDs := utils.UniqueUintSlice(req.ApprovableIds)
|
||||||
|
if len(approvableIDs) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range approvableIDs {
|
||||||
|
idCopy := id
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &idCopy, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
||||||
|
}
|
||||||
|
if latestApproval == nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for ProjectFlockKandang %d - chickins must be created first", id))
|
||||||
|
}
|
||||||
|
if latestApproval.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ProjectFlockKandang %d cannot be approved - current status is not in PENGAJUAN stage", id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step := utils.ProjectFlockKandangStepPengajuan
|
||||||
|
if action == entity.ApprovalActionApproved {
|
||||||
|
step = utils.ProjectFlockKandangStepDisetujui
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
||||||
|
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||||
|
|
||||||
|
for _, approvableID := range approvableIDs {
|
||||||
|
actorID := uint(1) // todo nanti ambil dari auth context
|
||||||
|
if _, err := approvalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlockKandang,
|
||||||
|
approvableID,
|
||||||
|
step,
|
||||||
|
&action,
|
||||||
|
actorID,
|
||||||
|
req.Notes,
|
||||||
|
); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == entity.ApprovalActionApproved {
|
||||||
|
chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get chickins for approval %d", approvableID))
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangForApproval, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang")
|
||||||
|
}
|
||||||
|
|
||||||
|
category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category))
|
||||||
|
|
||||||
|
if category == string(utils.ProjectFlockCategoryGrowing) {
|
||||||
|
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
|
||||||
|
}
|
||||||
|
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
|
||||||
|
}
|
||||||
|
} else if category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
|
||||||
|
}
|
||||||
|
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if action == entity.ApprovalActionRejected {
|
||||||
|
chickins, err := chickinRepoTx.GetPendingByProjectFlockKandangID(c.Context(), approvableID)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get pending chickins for rejection %d", approvableID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(chickins) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedQuantity := pw.Quantity + d.Quantity
|
kandangForRejection, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID)
|
||||||
if err := productWarehouseRepoTx.PatchOne(c.Context(), pw.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil {
|
if err != nil {
|
||||||
return false, err
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang")
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryForRejection := strings.ToUpper(strings.TrimSpace(kandangForRejection.ProjectFlock.Category))
|
||||||
|
|
||||||
|
for _, chickin := range chickins {
|
||||||
|
|
||||||
|
if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) {
|
||||||
|
updates := map[string]any{"quantity": gorm.Expr("quantity + ?", chickin.PendingUsageQty)}
|
||||||
|
|
||||||
|
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to restore product warehouse quantity for chickin %d", chickin.Id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil {
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to delete rejected chickin %d", chickin.Id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Delete(&entity.ProjectChickinDetail{}).Error; err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
restored, err := restoreFromDetails()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to restore from chickin details: %+v", err)
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
return rollback(err)
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !restored {
|
updated := make([]entity.ProjectChickin, 0)
|
||||||
|
for _, kandangID := range approvableIDs {
|
||||||
|
var chickins []entity.ProjectChickin
|
||||||
|
if err := s.Repository.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", kandangID).Find(&chickins).Error; err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load approved chickins")
|
||||||
|
}
|
||||||
|
updated = append(updated, chickins...)
|
||||||
|
}
|
||||||
|
|
||||||
projectflockkandang, err := pfkRepoTx.GetByID(c.Context(), population.ProjectFlockKandangId)
|
return updated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) {
|
||||||
|
|
||||||
|
products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
|
||||||
|
if err == nil && len(products) > 0 {
|
||||||
|
return &products[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get %s product: %w", categoryCode, err)
|
||||||
|
}
|
||||||
|
if product == nil {
|
||||||
|
return nil, fmt.Errorf("no %s product found in system", categoryCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
newPW := &entity.ProductWarehouse{
|
||||||
|
ProductId: product.Id,
|
||||||
|
WarehouseId: warehouseId,
|
||||||
|
Quantity: 0,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create %s product warehouse: %w", categoryCode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPW, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []entity.ProjectChickin, targetPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error {
|
||||||
|
|
||||||
|
if targetPW == nil || targetPW.Id == 0 {
|
||||||
|
return fmt.Errorf("invalid target product warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
||||||
|
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||||
|
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
|
||||||
|
|
||||||
|
for _, chickin := range chickins {
|
||||||
|
|
||||||
|
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get projectflock kandang: %+v", err)
|
return fmt.Errorf("failed to check population existence for chickin %d: %w", chickin.Id, err)
|
||||||
return rollback(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var warehouse entity.Warehouse
|
if populationExists {
|
||||||
if err := tx.WithContext(c.Context()).Where("kandang_id = ?", projectflockkandang.KandangId).First(&warehouse).Error; err != nil {
|
s.Log.Infof("population already exists for chickin %d, skipping", chickin.Id)
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
continue
|
||||||
return rollback(fiber.NewError(fiber.StatusNotFound, "Warehouse not found for kandang"))
|
}
|
||||||
|
|
||||||
|
quantityToConvert := chickin.PendingUsageQty
|
||||||
|
|
||||||
|
if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{
|
||||||
|
"usage_qty": quantityToConvert,
|
||||||
|
"pending_usage_qty": 0,
|
||||||
|
}, nil); err != nil {
|
||||||
|
return fmt.Errorf("failed to update chickin %d qty: %w", chickin.Id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if chickin.ProductWarehouseId != targetPW.Id {
|
||||||
|
if err := productWarehouseTx.PatchOne(ctx.Context(), chickin.ProductWarehouseId, map[string]any{
|
||||||
|
"quantity": gorm.Expr("quantity - ?", quantityToConvert),
|
||||||
|
}, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Source product warehouse %d not found", chickin.ProductWarehouseId))
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to deduct source warehouse quantity for chickin %d: %w", chickin.Id, err)
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to get warehouse: %+v", err)
|
|
||||||
return rollback(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
productWarehouse, err := s.ProductWarehouseRepo.GetLatestByCategoryCodeAndWarehouseID(
|
if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{
|
||||||
c.Context(),
|
"quantity": gorm.Expr("quantity + ?", quantityToConvert),
|
||||||
"DOC",
|
}, nil); err != nil {
|
||||||
warehouse.Id,
|
|
||||||
tx,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return rollback(fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse"))
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id))
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to get product warehouse: %+v", err)
|
return fmt.Errorf("failed to update target warehouse quantity: %w", err)
|
||||||
return rollback(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedQuantity := productWarehouse.Quantity + chickin.Quantity
|
population := &entity.ProjectFlockPopulation{
|
||||||
if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil {
|
ProjectChickinId: chickin.Id,
|
||||||
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
|
ProductWarehouseId: targetPW.Id,
|
||||||
return rollback(err)
|
TotalQty: quantityToConvert,
|
||||||
|
TotalUsedQty: 0,
|
||||||
|
Notes: chickin.Notes,
|
||||||
|
CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
}
|
if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil {
|
||||||
|
return err
|
||||||
// delete chickin (single place)
|
|
||||||
if err := chickinRepoTx.DeleteOne(c.Context(), id); err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found"))
|
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to delete chickin: %+v", err)
|
|
||||||
return rollback(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit().Error; err != nil {
|
|
||||||
s.Log.Errorf("Failed to commit transaction: %+v", err)
|
|
||||||
return rollback(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *chickinService) Approve(c *fiber.Ctx, id uint) error {
|
|
||||||
|
|
||||||
// todo: ini contoh akhir jika sudah approved
|
|
||||||
|
|
||||||
chickin, err := s.Repository.GetByID(
|
|
||||||
c.Context(),
|
|
||||||
id,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed get chickin by id: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
population, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), chickin.ProjectFlockKandangId)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to get project flock population: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), population.Id, map[string]any{
|
|
||||||
"reserved_quantity": population.ReservedQuantity - chickin.Quantity,
|
|
||||||
"initial_quantity": population.InitialQuantity + chickin.Quantity,
|
|
||||||
"current_quantity": population.CurrentQuantity + chickin.Quantity,
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to update project flock population: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package validation
|
|||||||
type Create struct {
|
type Create struct {
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
||||||
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
|
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
|
||||||
Note string `json:"note" validate:"omitempty`
|
Note string `json:"note" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
@@ -16,3 +16,9 @@ type Query struct {
|
|||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Approve struct {
|
||||||
|
Action string `json:"action" validate:"required_strict"`
|
||||||
|
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||||
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
|
}
|
||||||
|
|||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectFlockKandangController struct {
|
||||||
|
ProjectFlockKandangService service.ProjectFlockKandangService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProjectFlockKandangController(projectFlockKandangService service.ProjectFlockKandangService) *ProjectFlockKandangController {
|
||||||
|
return &ProjectFlockKandangController{
|
||||||
|
ProjectFlockKandangService: projectFlockKandangService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error {
|
||||||
|
query := &validation.Query{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Search: c.Query("search", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := u.ProjectFlockKandangService.GetAll(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.ProjectFlockKandangListDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get all projectFlockKandangs successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: dto.ToProjectFlockKandangListDTOs(result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, availableQtys, err := u.ProjectFlockKandangService.GetOne(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get projectFlockKandang successfully",
|
||||||
|
Data: dto.ToProjectFlockKandangListDTOWithAvailableQty(*result, availableQtys),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
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"
|
||||||
|
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
|
||||||
|
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||||
|
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||||
|
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
||||||
|
chickinDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto"
|
||||||
|
projectFlockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto"
|
||||||
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs (ordered) ===
|
||||||
|
|
||||||
|
type ProjectFlockKandangBaseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectFlockDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
|
||||||
|
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
|
||||||
|
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
|
||||||
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KandangDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductWarehouseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Product *productDTO.ProductBaseDTO `json:"product,omitempty"`
|
||||||
|
Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AvailableQtyDTO struct {
|
||||||
|
AvailableQty float64 `json:"available_qty"`
|
||||||
|
ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectFlockKandangListDTO struct {
|
||||||
|
ProjectFlockKandangBaseDTO
|
||||||
|
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
|
||||||
|
Kandang *KandangDTO `json:"kandang,omitempty"`
|
||||||
|
Chickins []chickinDTO.ChickinBaseDTO `json:"chickins,omitempty"`
|
||||||
|
AvailableQtys []AvailableQtyDTO `json:"available_qtys,omitempty"`
|
||||||
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectFlockKandangDetailDTO struct {
|
||||||
|
ProjectFlockKandangListDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions (ordered) ===
|
||||||
|
|
||||||
|
func ToProjectFlockKandangBaseDTO(e entity.ProjectFlockKandang) ProjectFlockKandangBaseDTO {
|
||||||
|
return ProjectFlockKandangBaseDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO {
|
||||||
|
if pf == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ProjectFlockDTO{
|
||||||
|
Id: pf.Id,
|
||||||
|
Period: pf.Period,
|
||||||
|
Area: pf.Area,
|
||||||
|
Category: pf.Category,
|
||||||
|
Fcr: pf.Fcr,
|
||||||
|
Location: pf.Location,
|
||||||
|
CreatedUser: pf.CreatedUser,
|
||||||
|
CreatedAt: pf.CreatedAt,
|
||||||
|
UpdatedAt: pf.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtysRaw []map[string]interface{}) ProjectFlockKandangListDTO {
|
||||||
|
var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO
|
||||||
|
if e.ProjectFlock.Id != 0 {
|
||||||
|
mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock)
|
||||||
|
projectFlockSummary = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProjectFlockKandangListDTO{
|
||||||
|
ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e),
|
||||||
|
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
|
||||||
|
Kandang: toKandangDTO(e.Kandang),
|
||||||
|
Chickins: toChickinDTOs(e.Chickins),
|
||||||
|
AvailableQtys: toAvailableQtyDTOsFromRaw(availableQtysRaw),
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
||||||
|
Approval: toApprovalDTO(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toKandangDTO(kandang entity.Kandang) *KandangDTO {
|
||||||
|
if kandang.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &KandangDTO{
|
||||||
|
Id: kandang.Id,
|
||||||
|
Name: kandang.Name,
|
||||||
|
Status: kandang.Status,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO {
|
||||||
|
if e.LatestApproval != nil {
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
||||||
|
return &mapped
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKandangListDTO {
|
||||||
|
var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO
|
||||||
|
if e.ProjectFlock.Id != 0 {
|
||||||
|
mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock)
|
||||||
|
projectFlockSummary = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProjectFlockKandangListDTO{
|
||||||
|
ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e),
|
||||||
|
ProjectFlock: toProjectFlockDTO(projectFlockSummary),
|
||||||
|
Kandang: toKandangDTO(e.Kandang),
|
||||||
|
Chickins: toChickinDTOs(e.Chickins),
|
||||||
|
AvailableQtys: toAvailableQtyDTOs(e.Chickins),
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
||||||
|
Approval: toApprovalDTO(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProjectFlockKandangListDTOs(e []entity.ProjectFlockKandang) []ProjectFlockKandangListDTO {
|
||||||
|
result := make([]ProjectFlockKandangListDTO, len(e))
|
||||||
|
for i, r := range e {
|
||||||
|
result[i] = ToProjectFlockKandangListDTO(r)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProjectFlockKandangDetailDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDetailDTO {
|
||||||
|
return ProjectFlockKandangDetailDTO{
|
||||||
|
ProjectFlockKandangListDTO: ToProjectFlockKandangListDTO(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Helper Functions (ordered) ===
|
||||||
|
|
||||||
|
func toProductWarehouseDTO(pwData map[string]interface{}) *ProductWarehouseDTO {
|
||||||
|
if pwData == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dto := &ProductWarehouseDTO{}
|
||||||
|
|
||||||
|
if id, ok := pwData["id"].(float64); ok {
|
||||||
|
dto.Id = uint(id)
|
||||||
|
} else if id, ok := pwData["id"].(uint); ok {
|
||||||
|
dto.Id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
if pData, ok := pwData["product"].(map[string]interface{}); ok {
|
||||||
|
dto.Product = toProductDTO(pData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wData, ok := pwData["warehouse"].(map[string]interface{}); ok {
|
||||||
|
dto.Warehouse = toWarehouseDTO(wData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto
|
||||||
|
}
|
||||||
|
|
||||||
|
func toProductDTO(pData map[string]interface{}) *productDTO.ProductBaseDTO {
|
||||||
|
if pData == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
product := &productDTO.ProductBaseDTO{}
|
||||||
|
if id, ok := pData["id"].(float64); ok {
|
||||||
|
product.Id = uint(id)
|
||||||
|
} else if id, ok := pData["id"].(uint); ok {
|
||||||
|
product.Id = id
|
||||||
|
}
|
||||||
|
if name, ok := pData["name"].(string); ok {
|
||||||
|
product.Name = name
|
||||||
|
}
|
||||||
|
return product
|
||||||
|
}
|
||||||
|
|
||||||
|
func toWarehouseDTO(wData map[string]interface{}) *warehouseDTO.WarehouseBaseDTO {
|
||||||
|
if wData == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
warehouse := &warehouseDTO.WarehouseBaseDTO{}
|
||||||
|
if id, ok := wData["id"].(float64); ok {
|
||||||
|
warehouse.Id = uint(id)
|
||||||
|
} else if id, ok := wData["id"].(uint); ok {
|
||||||
|
warehouse.Id = id
|
||||||
|
}
|
||||||
|
if name, ok := wData["name"].(string); ok {
|
||||||
|
warehouse.Name = name
|
||||||
|
}
|
||||||
|
if wType, ok := wData["type"].(string); ok {
|
||||||
|
warehouse.Type = wType
|
||||||
|
}
|
||||||
|
return warehouse
|
||||||
|
}
|
||||||
|
|
||||||
|
func toCreatedUserDTO(pf entity.ProjectFlock) *userDTO.UserBaseDTO {
|
||||||
|
if pf.CreatedUser.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserBaseDTO(pf.CreatedUser)
|
||||||
|
return &mapped
|
||||||
|
} else if pf.CreatedBy != 0 {
|
||||||
|
return &userDTO.UserBaseDTO{
|
||||||
|
Id: pf.CreatedBy,
|
||||||
|
IdUser: int64(pf.CreatedBy),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toChickinDTOs(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO {
|
||||||
|
if len(chickins) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]chickinDTO.ChickinBaseDTO, len(chickins))
|
||||||
|
for i, ch := range chickins {
|
||||||
|
result[i] = chickinDTO.ToChickinBaseDTO(ch)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func toAvailableQtyDTOs(chickins []entity.ProjectChickin) []AvailableQtyDTO {
|
||||||
|
if len(chickins) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
availableQtyMap := make(map[uint]AvailableQtyDTO)
|
||||||
|
for _, ch := range chickins {
|
||||||
|
if ch.ProductWarehouse == nil || ch.ProductWarehouse.Quantity <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := availableQtyMap[ch.ProductWarehouseId]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pwDTO := &ProductWarehouseDTO{
|
||||||
|
Id: ch.ProductWarehouse.Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch.ProductWarehouse.Product.Id != 0 {
|
||||||
|
pwDTO.Product = &productDTO.ProductBaseDTO{
|
||||||
|
Id: ch.ProductWarehouse.Product.Id,
|
||||||
|
Name: ch.ProductWarehouse.Product.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch.ProductWarehouse.Warehouse.Id != 0 {
|
||||||
|
pwDTO.Warehouse = &warehouseDTO.WarehouseBaseDTO{
|
||||||
|
Id: ch.ProductWarehouse.Warehouse.Id,
|
||||||
|
Name: ch.ProductWarehouse.Warehouse.Name,
|
||||||
|
Type: ch.ProductWarehouse.Warehouse.Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
availableQtyMap[ch.ProductWarehouseId] = AvailableQtyDTO{
|
||||||
|
ProductWarehouse: pwDTO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(availableQtyMap) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]AvailableQtyDTO, 0, len(availableQtyMap))
|
||||||
|
for _, v := range availableQtyMap {
|
||||||
|
result = append(result, v)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func toAvailableQtyDTOsFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO {
|
||||||
|
if len(availableQtysRaw) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]AvailableQtyDTO, len(availableQtysRaw))
|
||||||
|
for i, v := range availableQtysRaw {
|
||||||
|
pwData, ok := v["product_warehouse"].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pwDTO := toProductWarehouseDTO(pwData)
|
||||||
|
availableQty := 0.0
|
||||||
|
if qty, ok := v["available_qty"].(float64); ok {
|
||||||
|
availableQty = qty
|
||||||
|
}
|
||||||
|
|
||||||
|
result[i] = AvailableQtyDTO{
|
||||||
|
AvailableQty: availableQty,
|
||||||
|
ProductWarehouse: pwDTO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package project_flock_kandangs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
|
||||||
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
|
||||||
|
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"
|
||||||
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectFlockKandangModule struct{}
|
||||||
|
|
||||||
|
func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
|
projectFlockPopulationRepo := rProjectFlockKandang.NewProjectFlockPopulationRepository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
// register workflow steps for project flock kandang approvals
|
||||||
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, validate)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
ProjectFlockKandangRoutes(router, userService, projectFlockKandangService)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package project_flock_kandangs
|
||||||
|
|
||||||
|
import (
|
||||||
|
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/controllers"
|
||||||
|
projectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
|
||||||
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlockKandang.ProjectFlockKandangService) {
|
||||||
|
ctrl := controller.NewProjectFlockKandangController(s)
|
||||||
|
|
||||||
|
route := v1.Group("/project-flock-kandangs")
|
||||||
|
|
||||||
|
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||||
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
|
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||||
|
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||||
|
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||||
|
|
||||||
|
route.Get("/", ctrl.GetAll)
|
||||||
|
route.Get("/:id", ctrl.GetOne)
|
||||||
|
|
||||||
|
}
|
||||||
+195
@@ -0,0 +1,195 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
|
||||||
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectFlockKandangService interface {
|
||||||
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error)
|
||||||
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type projectFlockKandangService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
Repository repository.ProjectFlockKandangRepository
|
||||||
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
WarehouseRepo rWarehouse.WarehouseRepository
|
||||||
|
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||||
|
PopulationRepo repository.ProjectFlockPopulationRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService {
|
||||||
|
return &projectFlockKandangService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
Repository: repo,
|
||||||
|
ApprovalSvc: approvalSvc,
|
||||||
|
WarehouseRepo: warehouseRepo,
|
||||||
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
|
PopulationRepo: populationRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) {
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockKandangs, err := s.Repository.GetAll(c.Context())
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
total := int64(len(projectFlockKandangs))
|
||||||
|
|
||||||
|
return projectFlockKandangs, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) {
|
||||||
|
projectFlockKandang, err := s.Repository.GetByID(c.Context(), id)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed get projectFlockKandang by id: %+v", err)
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(projectFlockKandang.Chickins) > 0 && s.ApprovalSvc != nil {
|
||||||
|
latest, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch latest kandang approval for projectFlockKandang %d: %+v", projectFlockKandang.Id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if latest != nil {
|
||||||
|
projectFlockKandang.LatestApproval = latest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
availableQtys, err := s.getAvailableQuantities(c, projectFlockKandang)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch available quantities for kandang %d: %+v", projectFlockKandang.Kandang.Id, err)
|
||||||
|
availableQtys = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectFlockKandang, availableQtys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) ([]map[string]interface{}, error) {
|
||||||
|
if projectFlockKandang.Kandang.Id == 0 || s.WarehouseRepo == nil || s.ProductWarehouseRepo == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.Kandang.Id)
|
||||||
|
if err != nil || warehouse == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var productCategoryCode string
|
||||||
|
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
|
||||||
|
productCategoryCode = "DOC"
|
||||||
|
} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
productCategoryCode = "PULLET"
|
||||||
|
} else {
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
products, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id)
|
||||||
|
if err != nil || len(products) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []map[string]interface{}
|
||||||
|
for _, pw := range products {
|
||||||
|
availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only include product warehouse if available_qty > 0
|
||||||
|
if availableQty <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
productData := map[string]interface{}{
|
||||||
|
"id": pw.Product.Id,
|
||||||
|
"name": pw.Product.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
warehouseData := map[string]interface{}{
|
||||||
|
"id": pw.Warehouse.Id,
|
||||||
|
"name": pw.Warehouse.Name,
|
||||||
|
"type": pw.Warehouse.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
productWarehouseData := map[string]interface{}{
|
||||||
|
"id": pw.Id,
|
||||||
|
"product": productData,
|
||||||
|
"warehouse": warehouseData,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"available_qty": availableQty,
|
||||||
|
"product_warehouse": productWarehouseData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) {
|
||||||
|
availableQty := productWarehouse.Quantity
|
||||||
|
|
||||||
|
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
|
||||||
|
var totalPendingQty float64
|
||||||
|
|
||||||
|
for _, chickin := range projectFlockKandang.Chickins {
|
||||||
|
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
|
||||||
|
totalPendingQty += chickin.PendingUsageQty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
availableQty = productWarehouse.Quantity - totalPendingQty
|
||||||
|
if availableQty < 0 {
|
||||||
|
availableQty = 0
|
||||||
|
}
|
||||||
|
} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
|
var totalPopulation float64
|
||||||
|
var totalPendingQty float64
|
||||||
|
|
||||||
|
populations, err := s.PopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(c.Context(), projectFlockKandang.Id, productWarehouse.Id)
|
||||||
|
if err == nil {
|
||||||
|
for _, pop := range populations {
|
||||||
|
totalPopulation += pop.TotalQty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, chickin := range projectFlockKandang.Chickins {
|
||||||
|
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
|
||||||
|
totalPendingQty += chickin.PendingUsageQty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty
|
||||||
|
if availableQty < 0 {
|
||||||
|
availableQty = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableQty, nil
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
ProjectFlockId uint `json:"project_flock_id" validate:"required"`
|
||||||
|
KandangId uint `json:"kandang_id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
ProjectFlockId *uint `json:"project_flock_id,omitempty" validate:"omitempty"`
|
||||||
|
KandangId *uint `json:"kandang_id,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"`
|
||||||
|
}
|
||||||
@@ -85,6 +85,17 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var periodMap map[uint]int
|
||||||
|
if len(result) > 0 {
|
||||||
|
ids := make([]uint, len(result))
|
||||||
|
for i, item := range result {
|
||||||
|
ids[i] = item.Id
|
||||||
|
}
|
||||||
|
if periods, err := u.ProjectflockService.GetProjectPeriods(c, ids); err == nil {
|
||||||
|
periodMap = periods
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.SuccessWithPaginate[dto.ProjectFlockListDTO]{
|
JSON(response.SuccessWithPaginate[dto.ProjectFlockListDTO]{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
@@ -96,7 +107,7 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error {
|
|||||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
TotalResults: totalResults,
|
TotalResults: totalResults,
|
||||||
},
|
},
|
||||||
Data: dto.ToProjectFlockListDTOs(result),
|
Data: dto.ToProjectFlockListDTOsWithPeriods(result, periodMap),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,12 +124,19 @@ func (u *ProjectflockController) GetOne(c *fiber.Ctx) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var period int
|
||||||
|
if periods, err := u.ProjectflockService.GetProjectPeriods(c, []uint{uint(id)}); err == nil {
|
||||||
|
if p, ok := periods[uint(id)]; ok {
|
||||||
|
period = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.Success{
|
JSON(response.Success{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get projectflock successfully",
|
Message: "Get projectflock successfully",
|
||||||
Data: dto.ToProjectFlockListDTO(*result),
|
Data: dto.ToProjectFlockListDTOWithPeriod(*result, period),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,11 +223,29 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error {
|
|||||||
data interface{}
|
data interface{}
|
||||||
message = "Submit projectflock approval successfully"
|
message = "Submit projectflock approval successfully"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var periodMap map[uint]int
|
||||||
|
if len(results) > 0 {
|
||||||
|
ids := make([]uint, len(results))
|
||||||
|
for i, item := range results {
|
||||||
|
ids[i] = item.Id
|
||||||
|
}
|
||||||
|
if periods, err := u.ProjectflockService.GetProjectPeriods(c, ids); err == nil {
|
||||||
|
periodMap = periods
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(results) == 1 {
|
if len(results) == 1 {
|
||||||
data = dto.ToProjectFlockListDTO(results[0])
|
period := 0
|
||||||
|
if periodMap != nil {
|
||||||
|
if p, ok := periodMap[results[0].Id]; ok {
|
||||||
|
period = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data = dto.ToProjectFlockListDTOWithPeriod(results[0], period)
|
||||||
} else {
|
} else {
|
||||||
message = "Submit projectflock approvals successfully"
|
message = "Submit projectflock approvals successfully"
|
||||||
data = dto.ToProjectFlockListDTOs(results)
|
data = dto.ToProjectFlockListDTOsWithPeriods(results, periodMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
@@ -222,25 +258,32 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error {
|
func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error {
|
||||||
param := c.Params("project_flock_kandang_id")
|
param := c.Params("location_id")
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
id, err := strconv.Atoi(param)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id))
|
summaries, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
responseBody := dto.ToFlockPeriodSummaryDTO(summary.Flock, summary.NextPeriod)
|
responseBody := make([]dto.KandangPeriodSummaryDTO, 0, len(summaries))
|
||||||
|
for _, item := range summaries {
|
||||||
|
responseBody = append(responseBody, dto.KandangPeriodSummaryDTO{
|
||||||
|
Id: item.Id,
|
||||||
|
Name: item.Name,
|
||||||
|
Period: item.Period,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.Success{
|
JSON(response.Success{
|
||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get flock period summary successfully",
|
Message: "Get kandang period summary successfully",
|
||||||
Data: responseBody,
|
Data: responseBody,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
|
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
|
||||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/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"
|
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||||
|
|
||||||
// pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
// pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
@@ -22,6 +23,13 @@ type ProjectFlockBaseDTO struct {
|
|||||||
FlockName string `json:"flock_name"`
|
FlockName string `json:"flock_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KandangWithProjectFlockIdDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||||
|
}
|
||||||
|
|
||||||
type ProjectFlockListDTO struct {
|
type ProjectFlockListDTO struct {
|
||||||
ProjectFlockBaseDTO
|
ProjectFlockBaseDTO
|
||||||
// Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
|
// Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
|
||||||
@@ -45,7 +53,13 @@ type FlockPeriodDTO struct {
|
|||||||
NextPeriod int `json:"next_period"`
|
NextPeriod int `json:"next_period"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
|
type KandangPeriodSummaryDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Period int `json:"period"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectFlockListDTO {
|
||||||
var createdUser *userDTO.UserBaseDTO
|
var createdUser *userDTO.UserBaseDTO
|
||||||
if e.CreatedUser.Id != 0 {
|
if e.CreatedUser.Id != 0 {
|
||||||
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
|
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
|
||||||
@@ -91,31 +105,49 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ProjectFlockListDTO{
|
return ProjectFlockListDTO{
|
||||||
ProjectFlockBaseDTO: createProjectFlockBaseDTO(e),
|
ProjectFlockBaseDTO: createProjectFlockBaseDTO(e, period),
|
||||||
// Flock: flockSummary,
|
// Flock: flockSummary,
|
||||||
Area: areaSummary,
|
Area: areaSummary,
|
||||||
Kandangs: kandangSummaries,
|
Kandangs: kandangSummaries,
|
||||||
Category: e.Category,
|
Category: e.Category,
|
||||||
Fcr: fcrSummary,
|
Fcr: fcrSummary,
|
||||||
Location: locationSummary,
|
Location: locationSummary,
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
Approval: latestApproval,
|
Approval: latestApproval,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToProjectFlockListDTOs(items []entity.ProjectFlock) []ProjectFlockListDTO {
|
func ToProjectFlockListDTOs(items []entity.ProjectFlock) []ProjectFlockListDTO {
|
||||||
result := make([]ProjectFlockListDTO, len(items))
|
result := make([]ProjectFlockListDTO, len(items))
|
||||||
for i, item := range items {
|
for i, item := range items {
|
||||||
result[i] = ToProjectFlockListDTO(item)
|
result[i] = ToProjectFlockListDTOWithPeriod(item, 0)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
|
||||||
|
return ToProjectFlockListDTOWithPeriod(e, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProjectFlockListDTOsWithPeriods(items []entity.ProjectFlock, periods map[uint]int) []ProjectFlockListDTO {
|
||||||
|
result := make([]ProjectFlockListDTO, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
p := 0
|
||||||
|
if periods != nil {
|
||||||
|
if v, ok := periods[item.Id]; ok {
|
||||||
|
p = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result[i] = ToProjectFlockListDTOWithPeriod(item, p)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToProjectFlockDetailDTO(e entity.ProjectFlock) ProjectFlockDetailDTO {
|
func ToProjectFlockDetailDTO(e entity.ProjectFlock) ProjectFlockDetailDTO {
|
||||||
return ProjectFlockDetailDTO{
|
return ProjectFlockDetailDTO{
|
||||||
ProjectFlockListDTO: ToProjectFlockListDTO(e),
|
ProjectFlockListDTO: ToProjectFlockListDTOWithPeriod(e, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,10 +176,10 @@ func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.Approv
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func createProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO {
|
func createProjectFlockBaseDTO(e entity.ProjectFlock, period int) ProjectFlockBaseDTO {
|
||||||
return ProjectFlockBaseDTO{
|
return ProjectFlockBaseDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
Period: e.Period,
|
Period: period,
|
||||||
FlockName: e.FlockName,
|
FlockName: e.FlockName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
|
|||||||
pfLocal := ProjectFlockWithPivotDTO{
|
pfLocal := ProjectFlockWithPivotDTO{
|
||||||
ProjectFlockBaseDTO: ProjectFlockBaseDTO{
|
ProjectFlockBaseDTO: ProjectFlockBaseDTO{
|
||||||
Id: e.ProjectFlock.Id,
|
Id: e.ProjectFlock.Id,
|
||||||
Period: e.ProjectFlock.Period,
|
Period: e.Period,
|
||||||
FlockName: e.ProjectFlock.FlockName,
|
FlockName: e.ProjectFlock.FlockName,
|
||||||
},
|
},
|
||||||
Category: e.ProjectFlock.Category,
|
Category: e.ProjectFlock.Category,
|
||||||
|
|||||||
+65
-7
@@ -9,8 +9,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProjectFlockPopulationRepository interface {
|
type ProjectFlockPopulationRepository interface {
|
||||||
repository.BaseRepository[entity.ProjectFlockPopulation]
|
// domain-specific
|
||||||
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error)
|
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error)
|
||||||
|
ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error)
|
||||||
|
GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
|
||||||
|
GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
|
||||||
|
|
||||||
|
// subset of base repository methods used by services
|
||||||
|
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
|
||||||
|
// transaction helpers
|
||||||
|
WithTx(tx *gorm.DB) ProjectFlockPopulationRepository
|
||||||
|
DB() *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
type projectFlockPopulationRepositoryImpl struct {
|
type projectFlockPopulationRepositoryImpl struct {
|
||||||
@@ -23,13 +34,60 @@ func NewProjectFlockPopulationRepository(db *gorm.DB) ProjectFlockPopulationRepo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) {
|
func (r *projectFlockPopulationRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockPopulationRepository {
|
||||||
var record entity.ProjectFlockPopulation
|
return &projectFlockPopulationRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockPopulation](tx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockPopulationRepositoryImpl) DB() *gorm.DB {
|
||||||
|
return r.BaseRepositoryImpl.DB()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error) {
|
||||||
|
var records []entity.ProjectFlockPopulation
|
||||||
err := r.DB().WithContext(ctx).
|
err := r.DB().WithContext(ctx).
|
||||||
Where("project_flock_kandang_id = ?", projectFlockKandangID).
|
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
||||||
First(&record).Error
|
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Preload("ProjectChickin").
|
||||||
|
Find(&records).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &record, nil
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockPopulationRepositoryImpl) ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) {
|
||||||
|
var count int64
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Where("project_chickin_id = ?", projectChickinID).
|
||||||
|
Model(&entity.ProjectFlockPopulation{}).
|
||||||
|
Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockPopulationRepositoryImpl) GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) {
|
||||||
|
var records []entity.ProjectFlockPopulation
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Where("project_chickin_id = ? AND product_warehouse_id = ?", projectChickinID, productWarehouseID).
|
||||||
|
Find(&records).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) {
|
||||||
|
var records []entity.ProjectFlockPopulation
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
||||||
|
Where("project_chickins.project_flock_kandang_id = ? AND project_flock_populations.product_warehouse_id = ?", projectFlockKandangID, productWarehouseID).
|
||||||
|
Find(&records).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return records, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-69
@@ -2,7 +2,6 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -10,17 +9,12 @@ import (
|
|||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/clause"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const baseNameExpression = "LOWER(TRIM(regexp_replace(flock_name, '\\\\s+\\\\d+(\\\\s+\\\\d+)*$', '', 'g')))"
|
const baseNameExpression = "LOWER(TRIM(regexp_replace(flock_name, '\\\\s+\\\\d+(\\\\s+\\\\d+)*$', '', 'g')))"
|
||||||
|
|
||||||
type ProjectflockRepository interface {
|
type ProjectflockRepository interface {
|
||||||
repository.BaseRepository[entity.ProjectFlock]
|
repository.BaseRepository[entity.ProjectFlock]
|
||||||
GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error)
|
|
||||||
GetActiveByBaseName(ctx context.Context, baseName string) (*entity.ProjectFlock, error)
|
|
||||||
GetMaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
|
|
||||||
GetNextSequenceForBase(ctx context.Context, baseName string) (int, error)
|
|
||||||
GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error)
|
GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error)
|
||||||
WithDefaultRelations() func(*gorm.DB) *gorm.DB
|
WithDefaultRelations() func(*gorm.DB) *gorm.DB
|
||||||
ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error)
|
ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error)
|
||||||
@@ -39,65 +33,6 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProjectflockRepositoryImpl) GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error) {
|
|
||||||
var records []entity.ProjectFlock
|
|
||||||
if err := r.DB().WithContext(ctx).
|
|
||||||
Unscoped().
|
|
||||||
Where(baseNameExpression+" = LOWER(?)", baseName).
|
|
||||||
Order("period ASC").
|
|
||||||
Find(&records).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return records, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProjectflockRepositoryImpl) GetActiveByBaseName(ctx context.Context, baseName string) (*entity.ProjectFlock, error) {
|
|
||||||
var record entity.ProjectFlock
|
|
||||||
err := r.DB().WithContext(ctx).
|
|
||||||
Where(baseNameExpression+" = LOWER(?)", baseName).
|
|
||||||
Order("period DESC").
|
|
||||||
First(&record).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &record, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProjectflockRepositoryImpl) GetMaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) {
|
|
||||||
var max int
|
|
||||||
if err := r.DB().WithContext(ctx).
|
|
||||||
Model(&entity.ProjectFlock{}).
|
|
||||||
Where(baseNameExpression+" = LOWER(?)", baseName).
|
|
||||||
Select("COALESCE(MAX(period), 0)").
|
|
||||||
Scan(&max).Error; err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return max, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProjectflockRepositoryImpl) GetNextSequenceForBase(ctx context.Context, baseName string) (int, error) {
|
|
||||||
var payload struct {
|
|
||||||
Period int
|
|
||||||
}
|
|
||||||
if err := r.DB().WithContext(ctx).
|
|
||||||
Model(&entity.ProjectFlock{}).
|
|
||||||
Where(baseNameExpression+" = LOWER(?)", baseName).
|
|
||||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
||||||
Order("period DESC").
|
|
||||||
Limit(1).
|
|
||||||
Select("period").
|
|
||||||
Scan(&payload).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return 1, nil
|
|
||||||
}
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return payload.Period + 1, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
|
func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
|
||||||
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
|
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = r.withDefaultRelations(db)
|
db = r.withDefaultRelations(db)
|
||||||
@@ -132,7 +67,13 @@ func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *vali
|
|||||||
db = db.Where("project_flocks.location_id = ?", params.LocationId)
|
db = db.Where("project_flocks.location_id = ?", params.LocationId)
|
||||||
}
|
}
|
||||||
if params.Period > 0 {
|
if params.Period > 0 {
|
||||||
db = db.Where("project_flocks.period = ?", params.Period)
|
db = db.Where(`
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM project_flock_kandangs pfk
|
||||||
|
WHERE pfk.project_flock_id = project_flocks.id
|
||||||
|
AND pfk.period = ?
|
||||||
|
)`, params.Period)
|
||||||
}
|
}
|
||||||
if len(params.KandangIds) > 0 {
|
if len(params.KandangIds) > 0 {
|
||||||
db = db.Where(`
|
db = db.Where(`
|
||||||
@@ -179,10 +120,15 @@ func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch s
|
|||||||
OR LOWER(created_users.email) LIKE ?
|
OR LOWER(created_users.email) LIKE ?
|
||||||
OR LOWER(project_flocks.flock_name) LIKE ?
|
OR LOWER(project_flocks.flock_name) LIKE ?
|
||||||
OR LOWER(TRIM(regexp_replace(project_flocks.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))) LIKE ?
|
OR LOWER(TRIM(regexp_replace(project_flocks.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))) LIKE ?
|
||||||
OR LOWER(CAST(project_flocks.period AS TEXT)) LIKE ?
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM project_flock_kandangs
|
||||||
|
WHERE project_flock_kandangs.project_flock_id = project_flocks.id
|
||||||
|
AND LOWER(CAST(project_flock_kandangs.period AS TEXT)) LIKE ?
|
||||||
|
)
|
||||||
OR EXISTS (
|
OR EXISTS (
|
||||||
SELECT 1 FROM kandangs
|
SELECT 1 FROM kandangs
|
||||||
WHERE kandangs.project_flock_id = project_flocks.id
|
JOIN project_flock_kandangs pfk ON pfk.kandang_id = kandangs.id
|
||||||
|
WHERE pfk.project_flock_id = project_flocks.id
|
||||||
AND LOWER(kandangs.name) LIKE ?
|
AND LOWER(kandangs.name) LIKE ?
|
||||||
)
|
)
|
||||||
`,
|
`,
|
||||||
@@ -236,7 +182,7 @@ func (r *ProjectflockRepositoryImpl) buildOrderExpressions(sortBy, sortOrder str
|
|||||||
}
|
}
|
||||||
case "period":
|
case "period":
|
||||||
return []string{
|
return []string{
|
||||||
fmt.Sprintf("project_flocks.period %s", direction),
|
fmt.Sprintf("(SELECT COALESCE(MAX(period), 0) FROM project_flock_kandangs pfk WHERE pfk.project_flock_id = project_flocks.id) %s", direction),
|
||||||
fmt.Sprintf("project_flocks.id %s", direction),
|
fmt.Sprintf("project_flocks.id %s", direction),
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
+42
-1
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -18,7 +19,9 @@ type ProjectFlockKandangRepository interface {
|
|||||||
HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error)
|
HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error)
|
||||||
FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error)
|
FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error)
|
||||||
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
|
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
|
||||||
|
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
|
||||||
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
||||||
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
DB() *gorm.DB
|
DB() *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +62,9 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entit
|
|||||||
Preload("ProjectFlock.Kandangs").
|
Preload("ProjectFlock.Kandangs").
|
||||||
Preload("ProjectFlock.KandangHistory").
|
Preload("ProjectFlock.KandangHistory").
|
||||||
Preload("Kandang").
|
Preload("Kandang").
|
||||||
|
Preload("Chickins").
|
||||||
|
Preload("Chickins.CreatedUser").
|
||||||
|
Preload("Chickins.ProductWarehouse").
|
||||||
Order("project_flock_id ASC, created_at ASC").
|
Order("project_flock_id ASC, created_at ASC").
|
||||||
Find(&records).Error; err != nil {
|
Find(&records).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -73,6 +79,9 @@ func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKand
|
|||||||
func (r *projectFlockKandangRepositoryImpl) DB() *gorm.DB {
|
func (r *projectFlockKandangRepositoryImpl) DB() *gorm.DB {
|
||||||
return r.db
|
return r.db
|
||||||
}
|
}
|
||||||
|
func (r *projectFlockKandangRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return repository.Exists[entity.ProjectFlockKandang](ctx, r.db, id)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) {
|
func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) {
|
||||||
record := new(entity.ProjectFlockKandang)
|
record := new(entity.ProjectFlockKandang)
|
||||||
@@ -85,6 +94,9 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint
|
|||||||
Preload("ProjectFlock.Kandangs").
|
Preload("ProjectFlock.Kandangs").
|
||||||
Preload("ProjectFlock.KandangHistory").
|
Preload("ProjectFlock.KandangHistory").
|
||||||
Preload("Kandang").
|
Preload("Kandang").
|
||||||
|
Preload("Chickins").
|
||||||
|
Preload("Chickins.CreatedUser").
|
||||||
|
Preload("Chickins.ProductWarehouse").
|
||||||
First(record, id).Error; err != nil {
|
First(record, id).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -103,6 +115,9 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
|
|||||||
Preload("ProjectFlock.Kandangs").
|
Preload("ProjectFlock.Kandangs").
|
||||||
Preload("ProjectFlock.KandangHistory").
|
Preload("ProjectFlock.KandangHistory").
|
||||||
Preload("Kandang").
|
Preload("Kandang").
|
||||||
|
Preload("Chickins").
|
||||||
|
Preload("Chickins.CreatedUser").
|
||||||
|
Preload("Chickins.ProductWarehouse").
|
||||||
First(record).Error; err != nil {
|
First(record).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -163,7 +178,33 @@ func (r *projectFlockKandangRepositoryImpl) MaxPeriodByBaseName(ctx context.Cont
|
|||||||
Table("project_flock_kandangs pfk").
|
Table("project_flock_kandangs pfk").
|
||||||
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
|
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
|
||||||
Where(flockBaseNameExpression+" = LOWER(?)", baseName).
|
Where(flockBaseNameExpression+" = LOWER(?)", baseName).
|
||||||
Select("COALESCE(MAX(pf.period), 0)").
|
Select("COALESCE(MAX(pfk.period), 0)").
|
||||||
Scan(&max).Error
|
Scan(&max).Error
|
||||||
return max, err
|
return max, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockKandangRepositoryImpl) ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error) {
|
||||||
|
result := make(map[uint]int)
|
||||||
|
if len(projectIDs) == 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type row struct {
|
||||||
|
ProjectFlockID uint
|
||||||
|
Period int
|
||||||
|
}
|
||||||
|
var rows []row
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Table("project_flock_kandangs").
|
||||||
|
Where("project_flock_id IN ?", projectIDs).
|
||||||
|
Select("project_flock_id, COALESCE(MAX(period), 0) AS period").
|
||||||
|
Group("project_flock_id").
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range rows {
|
||||||
|
result[item.ProjectFlockID] = item.Period
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package project_flocks
|
package project_flocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/controllers"
|
||||||
projectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
projectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,7 +13,7 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
|
|||||||
ctrl := controller.NewProjectflockController(s)
|
ctrl := controller.NewProjectflockController(s)
|
||||||
|
|
||||||
route := v1.Group("/project-flocks")
|
route := v1.Group("/project-flocks")
|
||||||
route.Use(m.Auth(u))
|
// route.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
@@ -22,6 +22,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
|
|||||||
route.Delete("/:id", ctrl.DeleteOne)
|
route.Delete("/:id", ctrl.DeleteOne)
|
||||||
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
||||||
route.Post("/approvals", ctrl.Approval)
|
route.Post("/approvals", ctrl.Approval)
|
||||||
route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary)
|
route.Get("/kandangs/:location_id/periods", ctrl.GetFlockPeriodSummary)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ type ProjectflockService interface {
|
|||||||
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
|
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
|
||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
|
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
|
||||||
GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error)
|
GetFlockPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error)
|
||||||
|
GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error)
|
||||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,9 +53,10 @@ type projectflockService struct {
|
|||||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||||
}
|
}
|
||||||
|
|
||||||
type FlockPeriodSummary struct {
|
type KandangPeriodSummary struct {
|
||||||
Flock entity.Flock
|
Id uint
|
||||||
NextPeriod int
|
Name string
|
||||||
|
Period int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProjectflockService(
|
func NewProjectflockService(
|
||||||
@@ -81,6 +83,18 @@ func NewProjectflockService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("CreatedUser").
|
||||||
|
Preload("Flock").
|
||||||
|
Preload("Area").
|
||||||
|
Preload("Fcr").
|
||||||
|
Preload("Location").
|
||||||
|
Preload("Kandangs").
|
||||||
|
Preload("KandangHistory").
|
||||||
|
Preload("KandangHistory.Kandang")
|
||||||
|
}
|
||||||
|
|
||||||
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
|
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
@@ -206,6 +220,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
|||||||
if len(kandangs) != len(kandangIDs) {
|
if len(kandangs) != len(kandangIDs) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
||||||
}
|
}
|
||||||
|
for _, kandang := range kandangs {
|
||||||
|
if kandang.LocationId != req.LocationId {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id))
|
||||||
|
}
|
||||||
|
}
|
||||||
// larang kalau ada yg sudah terikat ke project lain
|
// larang kalau ada yg sudah terikat ke project lain
|
||||||
if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), kandangIDs, nil); err != nil {
|
if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), kandangIDs, nil); err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
|
||||||
@@ -224,22 +243,24 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
|||||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
projectRepo := repository.NewProjectflockRepository(dbTransaction)
|
projectRepo := repository.NewProjectflockRepository(dbTransaction)
|
||||||
|
|
||||||
nextSeq, err := projectRepo.GetNextSequenceForBase(c.Context(), canonicalBase)
|
// Generate unique flock name (sequential per base name, starting from 1)
|
||||||
if err != nil {
|
generatedName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, 1, nil)
|
||||||
return err
|
|
||||||
}
|
|
||||||
generatedName, seq, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, nextSeq, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
createBody.FlockName = generatedName
|
createBody.FlockName = generatedName
|
||||||
createBody.Period = seq
|
|
||||||
|
|
||||||
if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil {
|
if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs); err != nil {
|
// Compute period based on location history (max period in that location + 1),
|
||||||
|
// and store it on project_flock_kandangs only.
|
||||||
|
nextPeriod, err := s.nextLocationPeriod(c.Context(), dbTransaction, req.LocationId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs, nextPeriod); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,6 +396,15 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
|||||||
if len(kandangs) != len(newKandangIDs) {
|
if len(kandangs) != len(newKandangIDs) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
||||||
}
|
}
|
||||||
|
targetLocationID := existing.LocationId
|
||||||
|
if req.LocationId != nil && *req.LocationId > 0 {
|
||||||
|
targetLocationID = *req.LocationId
|
||||||
|
}
|
||||||
|
for _, kandang := range kandangs {
|
||||||
|
if kandang.LocationId != targetLocationID {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id))
|
||||||
|
}
|
||||||
|
}
|
||||||
if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), newKandangIDs, &id); err != nil {
|
if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), newKandangIDs, &id); err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
|
||||||
} else if linked {
|
} else if linked {
|
||||||
@@ -400,18 +430,11 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
|||||||
}
|
}
|
||||||
|
|
||||||
if needFlockNameRegenerate {
|
if needFlockNameRegenerate {
|
||||||
nextSeq, err := projectRepo.GetNextSequenceForBase(c.Context(), baseForGeneration)
|
newName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, baseForGeneration, 1, &id)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
newName, seq, err := s.generateSequentialFlockName(c.Context(), projectRepo, baseForGeneration, nextSeq, &id)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
updateBody["flock_name"] = newName
|
updateBody["flock_name"] = newName
|
||||||
if seq != existing.Period {
|
|
||||||
updateBody["period"] = seq
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(updateBody) > 0 {
|
if len(updateBody) > 0 {
|
||||||
@@ -455,7 +478,19 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(toAttach) > 0 {
|
if len(toAttach) > 0 {
|
||||||
if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach); err != nil {
|
var currentPeriod int
|
||||||
|
if err := dbTransaction.WithContext(c.Context()).
|
||||||
|
Table("project_flock_kandangs").
|
||||||
|
Where("project_flock_id = ?", id).
|
||||||
|
Select("COALESCE(MAX(period), 0)").
|
||||||
|
Scan(¤tPeriod).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if currentPeriod <= 0 {
|
||||||
|
currentPeriod = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach, currentPeriod); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -731,57 +766,90 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u
|
|||||||
return total, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, projectFlockKandangID uint) (*FlockPeriodSummary, error) {
|
// nextLocationPeriod computes the next period number for a given location
|
||||||
if projectFlockKandangID == 0 {
|
// based on the maximum period that has ever been used by any kandang in that location.
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
|
func (s projectflockService) nextLocationPeriod(ctx context.Context, tx *gorm.DB, locationID uint) (int, error) {
|
||||||
|
if locationID == 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, "location_id is required to compute period")
|
||||||
}
|
}
|
||||||
|
|
||||||
pivot, err := s.pivotRepo().GetByID(c.Context(), projectFlockKandangID)
|
db := s.Repository.DB()
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if tx != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
|
db = tx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var maxPeriod int
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Table("project_flock_kandangs pfk").
|
||||||
|
Joins("JOIN kandangs k ON k.id = pfk.kandang_id").
|
||||||
|
Where("k.location_id = ?", locationID).
|
||||||
|
Select("COALESCE(MAX(pfk.period), 0)").
|
||||||
|
Scan(&maxPeriod).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to compute max period for location %d: %+v", locationID, err)
|
||||||
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute period for location")
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxPeriod + 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) {
|
||||||
|
if len(projectIDs) == 0 {
|
||||||
|
return map[uint]int{}, nil
|
||||||
|
}
|
||||||
|
return s.pivotRepo().ProjectPeriodsByProjectIDs(c.Context(), projectIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) {
|
||||||
|
if locationID == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "location_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := s.Repository.LocationExists(c.Context(), locationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to fetch project_flock_kandang %d: %+v", projectFlockKandangID, err)
|
s.Log.Errorf("Failed to validate location %d: %+v", locationID, err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate location")
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Location not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
var baseName string
|
type kandangPeriodRow struct {
|
||||||
var referenceFlock *entity.Flock
|
Id uint
|
||||||
if pivot.ProjectFlock.Id != 0 {
|
Name string
|
||||||
baseName = pfutils.DeriveBaseName(pivot.ProjectFlock.FlockName)
|
LatestPeriod int
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(baseName) != "" {
|
var rows []kandangPeriodRow
|
||||||
referenceFlock, err = s.FlockRepo.GetByName(c.Context(), baseName)
|
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
db := s.Repository.DB().WithContext(c.Context())
|
||||||
s.Log.Errorf("Failed to fetch flock %q: %+v", baseName, err)
|
if err := db.
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock")
|
Table("kandangs AS k").
|
||||||
}
|
Select("k.id, k.name, COALESCE(MAX(pfk.period), 0) AS latest_period").
|
||||||
|
Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.kandang_id = k.id").
|
||||||
|
Where("k.location_id = ?", locationID).
|
||||||
|
Where("k.deleted_at IS NULL").
|
||||||
|
Group("k.id, k.name").
|
||||||
|
Order("k.id ASC").
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to fetch kandang period summary for location %d: %+v", locationID, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandang period summary")
|
||||||
}
|
}
|
||||||
|
|
||||||
if referenceFlock == nil {
|
summaries := make([]KandangPeriodSummary, 0, len(rows))
|
||||||
referenceFlock = &entity.Flock{Name: pivot.ProjectFlock.FlockName}
|
for _, row := range rows {
|
||||||
}
|
nextPeriod := 0
|
||||||
|
if row.LatestPeriod > 0 {
|
||||||
maxPeriod := pivot.ProjectFlock.Period
|
nextPeriod = row.LatestPeriod + 1
|
||||||
if strings.TrimSpace(baseName) != "" {
|
|
||||||
if headerMax, err := s.Repository.GetMaxPeriodByBaseName(c.Context(), baseName); err != nil {
|
|
||||||
s.Log.Warnf("Unable to compute header period for base %q: %+v", baseName, err)
|
|
||||||
} else if headerMax > maxPeriod {
|
|
||||||
maxPeriod = headerMax
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if pivotMax, err := s.pivotRepo().MaxPeriodByBaseName(c.Context(), baseName); err != nil {
|
summaries = append(summaries, KandangPeriodSummary{
|
||||||
s.Log.Warnf("Unable to compute pivot period for base %q: %+v", baseName, err)
|
Id: row.Id,
|
||||||
} else if pivotMax > maxPeriod {
|
Name: row.Name,
|
||||||
maxPeriod = pivotMax
|
Period: nextPeriod,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return &FlockPeriodSummary{
|
return summaries, nil
|
||||||
Flock: *referenceFlock,
|
|
||||||
NextPeriod: maxPeriod + 1,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func uniqueUintSlice(values []uint) []uint {
|
func uniqueUintSlice(values []uint) []uint {
|
||||||
@@ -857,7 +925,7 @@ func (s projectflockService) ensureFlockByName(ctx context.Context, actorID uint
|
|||||||
return newFlock, nil
|
return newFlock, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint) error {
|
func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, period int) error {
|
||||||
if len(kandangIDs) == 0 {
|
if len(kandangIDs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -895,6 +963,7 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
|
|||||||
records = append(records, &entity.ProjectFlockKandang{
|
records = append(records, &entity.ProjectFlockKandang{
|
||||||
ProjectFlockId: projectFlockID,
|
ProjectFlockId: projectFlockID,
|
||||||
KandangId: id,
|
KandangId: id,
|
||||||
|
Period: period,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err := s.pivotRepoWithTx(dbTransaction).CreateMany(ctx, records); err != nil {
|
if err := s.pivotRepoWithTx(dbTransaction).CreateMany(ctx, records); err != nil {
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package dto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||||
|
productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
@@ -17,19 +19,19 @@ type RecordingBaseDTO struct {
|
|||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||||
RecordDatetime time.Time `json:"record_datetime"`
|
RecordDatetime time.Time `json:"record_datetime"`
|
||||||
Day *int `json:"day,omitempty"`
|
Day int `json:"day"`
|
||||||
ProjectFlockCategory *string `json:"project_flock_category,omitempty"`
|
ProjectFlockCategory string `json:"project_flock_category"`
|
||||||
TotalDepletionQty *float64 `json:"total_depletion_qty,omitempty"`
|
TotalDepletionQty float64 `json:"total_depletion_qty"`
|
||||||
CumDepletionRate *float64 `json:"cum_depletion_rate,omitempty"`
|
CumDepletionRate float64 `json:"cum_depletion_rate"`
|
||||||
DailyGain *float64 `json:"daily_gain,omitempty"`
|
DailyGain float64 `json:"daily_gain"`
|
||||||
AvgDailyGain *float64 `json:"avg_daily_gain,omitempty"`
|
AvgDailyGain float64 `json:"avg_daily_gain"`
|
||||||
CumIntake *int `json:"cum_intake,omitempty"`
|
CumIntake int `json:"cum_intake"`
|
||||||
FcrValue *float64 `json:"fcr_value,omitempty"`
|
FcrValue float64 `json:"fcr_value"`
|
||||||
TotalChickQty *float64 `json:"total_chick_qty,omitempty"`
|
TotalChickQty float64 `json:"total_chick_qty"`
|
||||||
Approval approvalDTO.ApprovalBaseDTO `json:"approval"`
|
Approval approvalDTO.ApprovalBaseDTO `json:"approval"`
|
||||||
EggGradingStatus *string `json:"egg_grading_status,omitempty"`
|
EggGradingStatus *string `json:"egg_grading_status"`
|
||||||
EggGradingPendingQty *int `json:"egg_grading_pending_qty,omitempty"`
|
EggGradingPendingQty *int `json:"egg_grading_pending_qty"`
|
||||||
EggGradingCompletedQty *int `json:"egg_grading_completed_qty,omitempty"`
|
EggGradingCompletedQty *int `json:"egg_grading_completed_qty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingListDTO struct {
|
type RecordingListDTO struct {
|
||||||
@@ -54,31 +56,24 @@ type RecordingBodyWeightDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RecordingDepletionDTO struct {
|
type RecordingDepletionDTO struct {
|
||||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
Qty float64 `json:"qty"`
|
Qty float64 `json:"qty"`
|
||||||
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
|
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingStockDTO struct {
|
type RecordingStockDTO struct {
|
||||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
UsageAmount *float64 `json:"usage_amount,omitempty"`
|
UsageAmount float64 `json:"usage_amount"`
|
||||||
PendingQty *float64 `json:"pending_qty,omitempty"`
|
PendingQty *float64 `json:"pending_qty,omitempty"`
|
||||||
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
|
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingEggDTO struct {
|
type RecordingEggDTO struct {
|
||||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
Id uint `json:"id"`
|
||||||
Qty int `json:"qty"`
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
|
Qty int `json:"qty"`
|
||||||
Gradings []RecordingEggGradingDTO `json:"gradings,omitempty"`
|
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
|
||||||
}
|
Gradings []RecordingEggGradingDTO `json:"gradings,omitempty"`
|
||||||
|
|
||||||
type RecordingProductWarehouseDTO struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
ProductId uint `json:"product_id"`
|
|
||||||
ProductName string `json:"product_name"`
|
|
||||||
WarehouseId uint `json:"warehouse_id"`
|
|
||||||
WarehouseName string `json:"warehouse_name"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingEggGradingDTO struct {
|
type RecordingEggGradingDTO struct {
|
||||||
@@ -89,12 +84,46 @@ type RecordingEggGradingDTO struct {
|
|||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO {
|
func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO {
|
||||||
var projectFlockCategory *string
|
var (
|
||||||
|
projectFlockCategory string
|
||||||
|
day int
|
||||||
|
totalDepletionQty float64
|
||||||
|
cumDepletionRate float64
|
||||||
|
dailyGain float64
|
||||||
|
avgDailyGain float64
|
||||||
|
cumIntake int
|
||||||
|
fcrValue float64
|
||||||
|
totalChickQty float64
|
||||||
|
)
|
||||||
|
|
||||||
|
if e.Day != nil {
|
||||||
|
day = *e.Day
|
||||||
|
}
|
||||||
|
if e.TotalDepletionQty != nil {
|
||||||
|
totalDepletionQty = *e.TotalDepletionQty
|
||||||
|
}
|
||||||
|
if e.CumDepletionRate != nil {
|
||||||
|
cumDepletionRate = *e.CumDepletionRate
|
||||||
|
}
|
||||||
|
if e.DailyGain != nil {
|
||||||
|
dailyGain = *e.DailyGain
|
||||||
|
}
|
||||||
|
if e.AvgDailyGain != nil {
|
||||||
|
avgDailyGain = *e.AvgDailyGain
|
||||||
|
}
|
||||||
|
if e.CumIntake != nil {
|
||||||
|
cumIntake = *e.CumIntake
|
||||||
|
}
|
||||||
|
if e.FcrValue != nil {
|
||||||
|
fcrValue = *e.FcrValue
|
||||||
|
}
|
||||||
|
if e.TotalChickQty != nil {
|
||||||
|
totalChickQty = *e.TotalChickQty
|
||||||
|
}
|
||||||
|
|
||||||
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
||||||
category := e.ProjectFlockKandang.ProjectFlock.Category
|
category := e.ProjectFlockKandang.ProjectFlock.Category
|
||||||
if category != "" {
|
projectFlockCategory = category
|
||||||
projectFlockCategory = &category
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
latestApproval := defaultRecordingLatestApproval(e)
|
latestApproval := defaultRecordingLatestApproval(e)
|
||||||
@@ -109,15 +138,15 @@ func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO {
|
|||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
||||||
RecordDatetime: e.RecordDatetime,
|
RecordDatetime: e.RecordDatetime,
|
||||||
Day: e.Day,
|
Day: day,
|
||||||
ProjectFlockCategory: projectFlockCategory,
|
ProjectFlockCategory: projectFlockCategory,
|
||||||
TotalDepletionQty: e.TotalDepletionQty,
|
TotalDepletionQty: totalDepletionQty,
|
||||||
CumDepletionRate: e.CumDepletionRate,
|
CumDepletionRate: cumDepletionRate,
|
||||||
DailyGain: e.DailyGain,
|
DailyGain: dailyGain,
|
||||||
AvgDailyGain: e.AvgDailyGain,
|
AvgDailyGain: avgDailyGain,
|
||||||
CumIntake: e.CumIntake,
|
CumIntake: cumIntake,
|
||||||
FcrValue: e.FcrValue,
|
FcrValue: fcrValue,
|
||||||
TotalChickQty: e.TotalChickQty,
|
TotalChickQty: totalChickQty,
|
||||||
Approval: latestApproval,
|
Approval: latestApproval,
|
||||||
EggGradingStatus: gradingStatus,
|
EggGradingStatus: gradingStatus,
|
||||||
EggGradingPendingQty: gradingPending,
|
EggGradingPendingQty: gradingPending,
|
||||||
@@ -149,12 +178,21 @@ func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
|
func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
|
||||||
|
listDTO := ToRecordingListDTO(e)
|
||||||
|
|
||||||
|
var eggs []RecordingEggDTO
|
||||||
|
if strings.EqualFold(listDTO.ProjectFlockCategory, string(utils.ProjectFlockCategoryLaying)) {
|
||||||
|
eggs = ToRecordingEggDTOs(e.Eggs)
|
||||||
|
} else if len(e.Eggs) > 0 {
|
||||||
|
eggs = ToRecordingEggDTOs(e.Eggs)
|
||||||
|
}
|
||||||
|
|
||||||
return RecordingDetailDTO{
|
return RecordingDetailDTO{
|
||||||
RecordingListDTO: ToRecordingListDTO(e),
|
RecordingListDTO: listDTO,
|
||||||
BodyWeights: ToRecordingBodyWeightDTOs(e.BodyWeights),
|
BodyWeights: ToRecordingBodyWeightDTOs(e.BodyWeights),
|
||||||
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
||||||
Stocks: ToRecordingStockDTOs(e.Stocks),
|
Stocks: ToRecordingStockDTOs(e.Stocks),
|
||||||
Eggs: ToRecordingEggDTOs(e.Eggs),
|
Eggs: eggs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +214,7 @@ func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []Recordin
|
|||||||
result[i] = RecordingDepletionDTO{
|
result[i] = RecordingDepletionDTO{
|
||||||
ProductWarehouseId: d.ProductWarehouseId,
|
ProductWarehouseId: d.ProductWarehouseId,
|
||||||
Qty: d.Qty,
|
Qty: d.Qty,
|
||||||
ProductWarehouse: toRecordingProductWarehouseDTO(&d.ProductWarehouse),
|
ProductWarehouse: mapProductWarehouseDTO(&d.ProductWarehouse),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -185,11 +223,16 @@ func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []Recordin
|
|||||||
func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO {
|
func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO {
|
||||||
result := make([]RecordingStockDTO, len(stocks))
|
result := make([]RecordingStockDTO, len(stocks))
|
||||||
for i, s := range stocks {
|
for i, s := range stocks {
|
||||||
|
var usageAmount float64
|
||||||
|
if s.UsageQty != nil {
|
||||||
|
usageAmount = *s.UsageQty
|
||||||
|
}
|
||||||
|
|
||||||
result[i] = RecordingStockDTO{
|
result[i] = RecordingStockDTO{
|
||||||
ProductWarehouseId: s.ProductWarehouseId,
|
ProductWarehouseId: s.ProductWarehouseId,
|
||||||
UsageAmount: s.UsageQty,
|
UsageAmount: usageAmount,
|
||||||
PendingQty: s.PendingQty,
|
PendingQty: s.PendingQty,
|
||||||
ProductWarehouse: toRecordingProductWarehouseDTO(&s.ProductWarehouse),
|
ProductWarehouse: mapProductWarehouseDTO(&s.ProductWarehouse),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -199,9 +242,10 @@ func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO {
|
|||||||
result := make([]RecordingEggDTO, len(eggs))
|
result := make([]RecordingEggDTO, len(eggs))
|
||||||
for i, egg := range eggs {
|
for i, egg := range eggs {
|
||||||
result[i] = RecordingEggDTO{
|
result[i] = RecordingEggDTO{
|
||||||
|
Id: egg.Id,
|
||||||
ProductWarehouseId: egg.ProductWarehouseId,
|
ProductWarehouseId: egg.ProductWarehouseId,
|
||||||
Qty: egg.Qty,
|
Qty: egg.Qty,
|
||||||
ProductWarehouse: toRecordingProductWarehouseDTO(&egg.ProductWarehouse),
|
ProductWarehouse: mapProductWarehouseDTO(&egg.ProductWarehouse),
|
||||||
Gradings: ToRecordingEggGradingDTOs(egg.GradingEggs),
|
Gradings: ToRecordingEggGradingDTOs(egg.GradingEggs),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,25 +268,17 @@ func ToRecordingEggGradingDTOs(gradings []entity.GradingEgg) []RecordingEggGradi
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func toRecordingProductWarehouseDTO(pw *entity.ProductWarehouse) *RecordingProductWarehouseDTO {
|
func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.ProductWarehouseDTO {
|
||||||
if pw == nil || pw.Id == 0 {
|
if pw == nil {
|
||||||
return nil
|
return productWarehouseDTO.ProductWarehouseDTO{}
|
||||||
}
|
}
|
||||||
|
|
||||||
dto := RecordingProductWarehouseDTO{
|
mapped := productWarehouseDTO.ToProductWarehouseDTO(pw)
|
||||||
Id: pw.Id,
|
if mapped == nil {
|
||||||
ProductId: pw.ProductId,
|
return productWarehouseDTO.ProductWarehouseDTO{}
|
||||||
WarehouseId: pw.WarehouseId,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if pw.Product.Id != 0 {
|
return *mapped
|
||||||
dto.ProductName = pw.Product.Name
|
|
||||||
}
|
|
||||||
if pw.Warehouse.Id != 0 {
|
|
||||||
dto.WarehouseName = pw.Warehouse.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dto
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const goodEggProductWarehouseID uint = 5
|
const goodEggProductWarehouseID uint = 5
|
||||||
|
|||||||
@@ -254,18 +254,22 @@ func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFloc
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) {
|
func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) {
|
||||||
var population entity.ProjectFlockPopulation
|
var total float64
|
||||||
err := tx.
|
err := tx.
|
||||||
Where("project_flock_kandang_id = ?", projectFlockKandangId).
|
Table("project_flock_populations").
|
||||||
Order("created_at DESC").
|
Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0) AS total_qty").
|
||||||
First(&population).Error
|
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangId).
|
||||||
return 0, nil
|
Scan(&total).Error
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return int64(math.Round(population.InitialQuantity)), nil
|
|
||||||
|
if total < 0 {
|
||||||
|
total = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return int64(math.Round(total)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
|
func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||||
|
|||||||
@@ -261,6 +261,10 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.BodyWeights == nil && req.Stocks == nil && req.Depletions == nil && req.Eggs == nil {
|
||||||
|
return s.GetOne(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
|
|
||||||
var recordingEntity *entity.Recording
|
var recordingEntity *entity.Recording
|
||||||
@@ -277,12 +281,21 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
recordingEntity = recording
|
recordingEntity = recording
|
||||||
|
|
||||||
|
hasBodyChanges := req.BodyWeights != nil
|
||||||
|
hasStockChanges := req.Stocks != nil
|
||||||
|
hasDepletionChanges := req.Depletions != nil
|
||||||
|
hasEggChanges := req.Eggs != nil
|
||||||
|
|
||||||
|
if !hasBodyChanges && !hasStockChanges && !hasDepletionChanges && !hasEggChanges {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var category string
|
var category string
|
||||||
if recordingEntity.ProjectFlockKandang != nil && recordingEntity.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
if recordingEntity.ProjectFlockKandang != nil && recordingEntity.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
||||||
category = strings.ToUpper(recordingEntity.ProjectFlockKandang.ProjectFlock.Category)
|
category = strings.ToUpper(recordingEntity.ProjectFlockKandang.ProjectFlock.Category)
|
||||||
}
|
}
|
||||||
isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying))
|
isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying))
|
||||||
if req.Eggs != nil {
|
if hasEggChanges {
|
||||||
if !isLaying && len(req.Eggs) > 0 {
|
if !isLaying && len(req.Eggs) > 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
||||||
}
|
}
|
||||||
@@ -291,7 +304,29 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.BodyWeights != nil {
|
if hasStockChanges {
|
||||||
|
if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasDepletionChanges || hasEggChanges {
|
||||||
|
if err := s.ensureProductWarehousesExist(c, nil, req.Depletions, req.Eggs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasExistingGradings := false
|
||||||
|
for _, egg := range recordingEntity.Eggs {
|
||||||
|
if len(egg.GradingEggs) > 0 {
|
||||||
|
hasExistingGradings = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasEggsAfterUpdate := len(recordingEntity.Eggs) > 0
|
||||||
|
|
||||||
|
if hasBodyChanges {
|
||||||
if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil {
|
if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil {
|
||||||
s.Log.Errorf("Failed to clear body weights: %+v", err)
|
s.Log.Errorf("Failed to clear body weights: %+v", err)
|
||||||
return err
|
return err
|
||||||
@@ -302,11 +337,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Stocks != nil {
|
if hasStockChanges {
|
||||||
if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id)
|
existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to list existing stocks: %+v", err)
|
s.Log.Errorf("Failed to list existing stocks: %+v", err)
|
||||||
@@ -330,17 +361,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Eggs != nil && req.Depletions == nil {
|
if hasDepletionChanges {
|
||||||
if err := s.ensureProductWarehousesExist(c, nil, nil, req.Eggs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Depletions != nil {
|
|
||||||
if err := s.ensureProductWarehousesExist(c, nil, req.Depletions, req.Eggs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
existingDepletions, err := s.Repository.ListDepletions(tx, recordingEntity.Id)
|
existingDepletions, err := s.Repository.ListDepletions(tx, recordingEntity.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to list existing depletions: %+v", err)
|
s.Log.Errorf("Failed to list existing depletions: %+v", err)
|
||||||
@@ -364,7 +385,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Eggs != nil {
|
if hasEggChanges {
|
||||||
existingEggs, err := s.Repository.ListEggs(tx, recordingEntity.Id)
|
existingEggs, err := s.Repository.ListEggs(tx, recordingEntity.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to list existing eggs: %+v", err)
|
s.Log.Errorf("Failed to list existing eggs: %+v", err)
|
||||||
@@ -386,17 +407,71 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err)
|
s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasExistingGradings = false
|
||||||
|
hasEggsAfterUpdate = len(req.Eggs) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
|
if hasBodyChanges || hasStockChanges || hasDepletionChanges {
|
||||||
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
|
||||||
return err
|
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
action := entity.ApprovalActionUpdated
|
action := entity.ApprovalActionUpdated
|
||||||
if err := s.createRecordingApproval(ctx, tx, recordingEntity.Id, utils.RecordingStepPengajuan, action, recordingEntity.CreatedBy, nil); err != nil {
|
actorID := recordingEntity.CreatedBy
|
||||||
s.Log.Errorf("Failed to create approval after recording update %d: %+v", recordingEntity.Id, err)
|
if actorID == 0 {
|
||||||
return err
|
actorID = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var step approvalutils.ApprovalStep
|
||||||
|
if isLaying {
|
||||||
|
if !hasEggsAfterUpdate {
|
||||||
|
step = utils.RecordingStepGradingTelur
|
||||||
|
} else if hasEggChanges {
|
||||||
|
step = utils.RecordingStepGradingTelur
|
||||||
|
} else if hasExistingGradings {
|
||||||
|
step = utils.RecordingStepPengajuan
|
||||||
|
} else {
|
||||||
|
step = utils.RecordingStepGradingTelur
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
step = utils.RecordingStepPengajuan
|
||||||
|
}
|
||||||
|
|
||||||
|
latestApproval := recordingEntity.LatestApproval
|
||||||
|
if latestApproval == nil {
|
||||||
|
if s.ApprovalSvc != nil {
|
||||||
|
if fetched, fetchErr := s.ApprovalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowRecording, recordingEntity.Id, nil); fetchErr != nil {
|
||||||
|
s.Log.Errorf("Failed to load latest approval for recording %d: %+v", recordingEntity.Id, fetchErr)
|
||||||
|
return fetchErr
|
||||||
|
} else {
|
||||||
|
latestApproval = fetched
|
||||||
|
}
|
||||||
|
} else if s.ApprovalRepo != nil {
|
||||||
|
if fetched, fetchErr := s.ApprovalRepo.LatestByTarget(ctx, utils.ApprovalWorkflowRecording.String(), recordingEntity.Id, nil); fetchErr != nil {
|
||||||
|
s.Log.Errorf("Failed to load latest approval for recording %d: %+v", recordingEntity.Id, fetchErr)
|
||||||
|
return fetchErr
|
||||||
|
} else {
|
||||||
|
latestApproval = fetched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldCreateApproval := true
|
||||||
|
if latestApproval != nil &&
|
||||||
|
latestApproval.StepNumber == uint16(step) &&
|
||||||
|
latestApproval.Action != nil &&
|
||||||
|
*latestApproval.Action == action {
|
||||||
|
shouldCreateApproval = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldCreateApproval {
|
||||||
|
if err := s.createRecordingApproval(ctx, tx, recordingEntity.Id, step, action, actorID, nil); err != nil {
|
||||||
|
s.Log.Errorf("Failed to create approval after recording update %d: %+v", recordingEntity.Id, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -1015,13 +1090,21 @@ func (s *recordingService) ensureChickInExists(ctx context.Context, projectFlock
|
|||||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
|
return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
|
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
return 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 errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock belum melakukan chick in sehingga belum dapat membuat recording")
|
if len(populations) == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Project flock belum memiliki chick in yang disetujui sehingga belum dapat membuat recording")
|
||||||
}
|
}
|
||||||
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")
|
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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ type (
|
|||||||
BodyWeight struct {
|
BodyWeight struct {
|
||||||
AvgWeight float64 `json:"avg_weight" validate:"required"`
|
AvgWeight float64 `json:"avg_weight" validate:"required"`
|
||||||
Qty float64 `json:"qty" validate:"required,gt=0"`
|
Qty float64 `json:"qty" validate:"required,gt=0"`
|
||||||
TotalWeight *float64 `json:"total_weight,omitempty" validate:"omitempty,gt=0"`
|
TotalWeight *float64 `json:"total_weight,omitempty" validate:"omitempty,gte=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
Stock struct {
|
Stock struct {
|
||||||
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||||
Qty *float64 `json:"qty,omitempty" validate:"required_without=UsageAmount,gte=0"`
|
Qty float64 `json:"qty" validate:"required,gte=0"`
|
||||||
PendingQty *float64 `json:"pending_qty,omitempty" validate:"omitempty,gte=0"`
|
PendingQty *float64 `json:"pending_qty,omitempty" validate:"omitempty,gte=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,10 +26,10 @@ type (
|
|||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
||||||
BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"`
|
BodyWeights []BodyWeight `json:"body_weights" validate:"dive"`
|
||||||
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
Stocks []Stock `json:"stocks" validate:"dive"`
|
||||||
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
Depletions []Depletion `json:"depletions" validate:"dive"`
|
||||||
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
|
Eggs []Egg `json:"eggs" validate:"omitempty,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins"
|
chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins"
|
||||||
projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks"
|
projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks"
|
||||||
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
|
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
|
||||||
|
transferLayings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings"
|
||||||
|
projectFlockKandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs"
|
||||||
// MODULE IMPORTS
|
// MODULE IMPORTS
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,8 +22,10 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
|
|||||||
projectflocks.ProjectflockModule{},
|
projectflocks.ProjectflockModule{},
|
||||||
recordings.RecordingModule{},
|
recordings.RecordingModule{},
|
||||||
chickins.ChickinModule{},
|
chickins.ChickinModule{},
|
||||||
|
transferLayings.TransferLayingModule{},
|
||||||
|
projectFlockKandangs.ProjectFlockKandangModule{},
|
||||||
// MODULE REGISTRY
|
// MODULE REGISTRY
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, m := range allModules {
|
for _, m := range allModules {
|
||||||
m.RegisterRoutes(group, db, validate)
|
m.RegisterRoutes(group, db, validate)
|
||||||
|
|||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TransferLayingController struct {
|
||||||
|
TransferLayingService service.TransferLayingService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTransferLayingController(transferLayingService service.TransferLayingService) *TransferLayingController {
|
||||||
|
return &TransferLayingController{
|
||||||
|
TransferLayingService: transferLayingService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
|
||||||
|
query := &validation.Query{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
SourceProjectFlockId: uint(c.QueryInt("source_project_flock_id", 0)),
|
||||||
|
TargetProjectFlockId: uint(c.QueryInt("target_project_flock_id", 0)),
|
||||||
|
TransferDateFrom: c.Query("transfer_date_from", ""),
|
||||||
|
TransferDateTo: c.Query("transfer_date_to", ""),
|
||||||
|
ApprovalStatus: c.Query("approval_status", ""),
|
||||||
|
TransferNumber: c.Query("transfer_number", ""),
|
||||||
|
Sort: c.Query("sort", "created_at"),
|
||||||
|
Order: c.Query("order", "desc"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, totalResults, err := u.TransferLayingService.GetAll(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.TransferLayingListDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get all transferLayings successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: query.Page,
|
||||||
|
Limit: query.Limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||||
|
TotalResults: totalResults,
|
||||||
|
},
|
||||||
|
Data: dto.ToTransferLayingListDTOs(result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TransferLayingController) GetOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, approval, err := u.TransferLayingService.GetOneWithApproval(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get transferLaying successfully",
|
||||||
|
Data: dto.ToTransferLayingDetailDTOWithSingleApproval(*result, approval),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TransferLayingController) CreateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Create)
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.TransferLayingService.CreateOne(c, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusCreated,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Create transferLaying successfully",
|
||||||
|
Data: dto.ToTransferLayingListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TransferLayingController) UpdateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Update)
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.TransferLayingService.UpdateOne(c, req, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Update transferLaying successfully",
|
||||||
|
Data: dto.ToTransferLayingListDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TransferLayingController) DeleteOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.TransferLayingService.DeleteOne(c, uint(id)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Common{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Delete transferLaying successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *TransferLayingController) Approval(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.Approve)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := u.TransferLayingService.Approval(c, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
data interface{}
|
||||||
|
message = "Submit transfer laying approval successfully"
|
||||||
|
)
|
||||||
|
if len(results) == 1 {
|
||||||
|
data = dto.ToTransferLayingListDTO(results[0])
|
||||||
|
} else {
|
||||||
|
message = "Submit transfer laying approvals successfully"
|
||||||
|
data = dto.ToTransferLayingListDTOs(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: message,
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||||
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// === DTO Structs ===
|
||||||
|
|
||||||
|
type TransferLayingBaseDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
TransferNumber string `json:"transfer_number"`
|
||||||
|
TransferDate time.Time `json:"transfer_date"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectFlockSummaryDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductSummaryDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WarehouseSummaryDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductWarehouseSummaryDTO struct {
|
||||||
|
Product *ProductSummaryDTO `json:"product,omitempty"`
|
||||||
|
Warehouse *WarehouseSummaryDTO `json:"warehouse,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectFlockKandangSummaryDTO struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
KandangId uint `json:"kandang_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayingTransferSourceDTO struct {
|
||||||
|
SourceProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"source_project_flock_kandang,omitempty"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"`
|
||||||
|
Note string `json:"note,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayingTransferTargetDTO struct {
|
||||||
|
TargetProjectFlockKandang *ProjectFlockKandangSummaryDTO `json:"target_project_flock_kandang,omitempty"`
|
||||||
|
Qty float64 `json:"qty"`
|
||||||
|
ProductWarehouse *ProductWarehouseSummaryDTO `json:"product_warehouse,omitempty"`
|
||||||
|
Note string `json:"note,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransferLayingListDTO struct {
|
||||||
|
TransferLayingBaseDTO
|
||||||
|
FromProjectFlock *ProjectFlockSummaryDTO `json:"from_project_flock,omitempty"`
|
||||||
|
ToProjectFlock *ProjectFlockSummaryDTO `json:"to_project_flock,omitempty"`
|
||||||
|
PendingUsageQty *float64 `json:"pending_usage_qty"`
|
||||||
|
UsageQty *float64 `json:"usage_qty"`
|
||||||
|
CreatedBy uint `json:"created_by"`
|
||||||
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransferLayingDetailDTO struct {
|
||||||
|
TransferLayingListDTO
|
||||||
|
Sources []LayingTransferSourceDTO `json:"sources,omitempty"`
|
||||||
|
Targets []LayingTransferTargetDTO `json:"targets,omitempty"`
|
||||||
|
Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Mapper Functions ===
|
||||||
|
|
||||||
|
func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO {
|
||||||
|
if pf == nil || pf.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ProjectFlockSummaryDTO{
|
||||||
|
Id: pf.Id,
|
||||||
|
Category: pf.Category,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProjectFlockKandangSummaryDTO(pfk *entity.ProjectFlockKandang) *ProjectFlockKandangSummaryDTO {
|
||||||
|
if pfk == nil || pfk.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ProjectFlockKandangSummaryDTO{
|
||||||
|
Id: pfk.Id,
|
||||||
|
KandangId: pfk.KandangId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProductSummaryDTO(product *entity.Product) *ProductSummaryDTO {
|
||||||
|
if product == nil || product.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ProductSummaryDTO{
|
||||||
|
Id: product.Id,
|
||||||
|
Name: product.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToWarehouseSummaryDTO(warehouse *entity.Warehouse) *WarehouseSummaryDTO {
|
||||||
|
if warehouse == nil || warehouse.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &WarehouseSummaryDTO{
|
||||||
|
Id: warehouse.Id,
|
||||||
|
Name: warehouse.Name,
|
||||||
|
Type: warehouse.Type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToProductWarehouseSummaryDTO(pw *entity.ProductWarehouse) *ProductWarehouseSummaryDTO {
|
||||||
|
if pw == nil || pw.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ProductWarehouseSummaryDTO{
|
||||||
|
Product: ToProductSummaryDTO(&pw.Product),
|
||||||
|
Warehouse: ToWarehouseSummaryDTO(&pw.Warehouse),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToLayingTransferSourceDTO(source entity.LayingTransferSource) LayingTransferSourceDTO {
|
||||||
|
return LayingTransferSourceDTO{
|
||||||
|
SourceProjectFlockKandang: ToProjectFlockKandangSummaryDTO(source.SourceProjectFlockKandang),
|
||||||
|
Qty: source.Qty,
|
||||||
|
ProductWarehouse: ToProductWarehouseSummaryDTO(source.ProductWarehouse),
|
||||||
|
Note: source.Note,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToLayingTransferSourceDTOs(sources []entity.LayingTransferSource) []LayingTransferSourceDTO {
|
||||||
|
if len(sources) == 0 {
|
||||||
|
return []LayingTransferSourceDTO{}
|
||||||
|
}
|
||||||
|
result := make([]LayingTransferSourceDTO, len(sources))
|
||||||
|
for i, s := range sources {
|
||||||
|
result[i] = ToLayingTransferSourceDTO(s)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToLayingTransferTargetDTO(target entity.LayingTransferTarget) LayingTransferTargetDTO {
|
||||||
|
return LayingTransferTargetDTO{
|
||||||
|
TargetProjectFlockKandang: ToProjectFlockKandangSummaryDTO(target.TargetProjectFlockKandang),
|
||||||
|
Qty: target.Qty,
|
||||||
|
ProductWarehouse: ToProductWarehouseSummaryDTO(target.ProductWarehouse),
|
||||||
|
Note: target.Note,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToLayingTransferTargetDTOs(targets []entity.LayingTransferTarget) []LayingTransferTargetDTO {
|
||||||
|
if len(targets) == 0 {
|
||||||
|
return []LayingTransferTargetDTO{}
|
||||||
|
}
|
||||||
|
result := make([]LayingTransferTargetDTO, len(targets))
|
||||||
|
for i, t := range targets {
|
||||||
|
result[i] = ToLayingTransferTargetDTO(t)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToTransferLayingBaseDTO(e entity.LayingTransfer) TransferLayingBaseDTO {
|
||||||
|
return TransferLayingBaseDTO{
|
||||||
|
Id: e.Id,
|
||||||
|
TransferNumber: e.TransferNumber,
|
||||||
|
TransferDate: e.TransferDate,
|
||||||
|
Notes: e.Notes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
|
||||||
|
var createdUser *userDTO.UserBaseDTO
|
||||||
|
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
|
||||||
|
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
|
||||||
|
createdUser = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return TransferLayingListDTO{
|
||||||
|
TransferLayingBaseDTO: ToTransferLayingBaseDTO(e),
|
||||||
|
FromProjectFlock: ToProjectFlockSummaryDTO(e.FromProjectFlock),
|
||||||
|
ToProjectFlock: ToProjectFlockSummaryDTO(e.ToProjectFlock),
|
||||||
|
PendingUsageQty: e.PendingUsageQty,
|
||||||
|
UsageQty: e.UsageQty,
|
||||||
|
CreatedBy: e.CreatedBy,
|
||||||
|
CreatedUser: createdUser,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO {
|
||||||
|
var latestApproval *approvalDTO.ApprovalBaseDTO
|
||||||
|
|
||||||
|
// Use LatestApproval from entity if available
|
||||||
|
if e.LatestApproval != nil {
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
||||||
|
latestApproval = &mapped
|
||||||
|
} else if len(approvals) > 0 {
|
||||||
|
// Fallback to approvals slice
|
||||||
|
latest := approvalDTO.ToApprovalDTO(approvals[len(approvals)-1])
|
||||||
|
latestApproval = &latest
|
||||||
|
}
|
||||||
|
|
||||||
|
return TransferLayingDetailDTO{
|
||||||
|
TransferLayingListDTO: ToTransferLayingListDTO(e),
|
||||||
|
Sources: ToLayingTransferSourceDTOs(e.Sources),
|
||||||
|
Targets: ToLayingTransferTargetDTOs(e.Targets),
|
||||||
|
Approval: latestApproval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToTransferLayingDetailDTOWithSingleApproval(e entity.LayingTransfer, approval *entity.Approval) TransferLayingDetailDTO {
|
||||||
|
var mappedApproval *approvalDTO.ApprovalBaseDTO
|
||||||
|
|
||||||
|
// Prefer LatestApproval from entity
|
||||||
|
if e.LatestApproval != nil && e.LatestApproval.Id != 0 {
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
||||||
|
mappedApproval = &mapped
|
||||||
|
} else if approval != nil && approval.Id != 0 {
|
||||||
|
// Fallback to passed approval parameter
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*approval)
|
||||||
|
mappedApproval = &mapped
|
||||||
|
}
|
||||||
|
|
||||||
|
return TransferLayingDetailDTO{
|
||||||
|
TransferLayingListDTO: ToTransferLayingListDTO(e),
|
||||||
|
Sources: ToLayingTransferSourceDTOs(e.Sources),
|
||||||
|
Targets: ToLayingTransferTargetDTOs(e.Targets),
|
||||||
|
Approval: mappedApproval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToTransferLayingListDTOs(items []entity.LayingTransfer) []TransferLayingListDTO {
|
||||||
|
result := make([]TransferLayingListDTO, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
result[i] = ToTransferLayingListDTO(item)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package transfer_layings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
||||||
|
sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TransferLayingModule struct{}
|
||||||
|
|
||||||
|
func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db)
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||||
|
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
||||||
|
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
|
||||||
|
productWarehouseRepo := rInventory.NewProductWarehouseRepository(db)
|
||||||
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
|
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowTransferToLaying, utils.TransferToLayingApprovalSteps); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to register transfer to laying approval workflow: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
transferLayingService := sTransferLaying.NewTransferLayingService(
|
||||||
|
transferLayingRepo,
|
||||||
|
projectFlockRepo,
|
||||||
|
projectFlockKandangRepo,
|
||||||
|
projectFlockPopulationRepo,
|
||||||
|
productWarehouseRepo,
|
||||||
|
warehouseRepo,
|
||||||
|
approvalService,
|
||||||
|
validate,
|
||||||
|
)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
TransferLayingRoutes(router, userService, transferLayingService)
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TransferLayingRepository interface {
|
||||||
|
repository.BaseRepository[entity.LayingTransfer]
|
||||||
|
GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error)
|
||||||
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransferLayingRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.LayingTransfer]
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository {
|
||||||
|
return &TransferLayingRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransfer](db),
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (r *TransferLayingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return repository.Exists[entity.LayingTransfer](ctx, r.db, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TransferLayingRepositoryImpl) GetByTransferNumber(ctx context.Context, transferNumber string) (*entity.LayingTransfer, error) {
|
||||||
|
var transfer entity.LayingTransfer
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("transfer_number = ?", transferNumber).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
First(&transfer).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &transfer, nil
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayingTransferSourceRepository interface {
|
||||||
|
repository.BaseRepository[entity.LayingTransferSource]
|
||||||
|
GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferSource, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayingTransferSourceRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.LayingTransferSource]
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLayingTransferSourceRepository(db *gorm.DB) LayingTransferSourceRepository {
|
||||||
|
return &LayingTransferSourceRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransferSource](db),
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LayingTransferSourceRepositoryImpl) GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferSource, error) {
|
||||||
|
var sources []entity.LayingTransferSource
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("laying_transfer_id = ?", layingTransferId).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&sources).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sources, nil
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayingTransferTargetRepository interface {
|
||||||
|
repository.BaseRepository[entity.LayingTransferTarget]
|
||||||
|
GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferTarget, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayingTransferTargetRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.LayingTransferTarget]
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLayingTransferTargetRepository(db *gorm.DB) LayingTransferTargetRepository {
|
||||||
|
return &LayingTransferTargetRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransferTarget](db),
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LayingTransferTargetRepositoryImpl) GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferTarget, error) {
|
||||||
|
var targets []entity.LayingTransferTarget
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("laying_transfer_id = ?", layingTransferId).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&targets).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return targets, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package transfer_layings
|
||||||
|
|
||||||
|
import (
|
||||||
|
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/controllers"
|
||||||
|
transferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
|
||||||
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.TransferLayingService) {
|
||||||
|
ctrl := controller.NewTransferLayingController(s)
|
||||||
|
|
||||||
|
route := v1.Group("/transfer_layings")
|
||||||
|
|
||||||
|
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||||
|
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||||
|
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
|
||||||
|
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
|
||||||
|
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||||
|
// route.Post("/approval", m.Auth(u), ctrl.Approval)
|
||||||
|
|
||||||
|
route.Get("/", ctrl.GetAll)
|
||||||
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
route.Get("/:id", ctrl.GetOne)
|
||||||
|
route.Patch("/:id", ctrl.UpdateOne)
|
||||||
|
route.Delete("/:id", ctrl.DeleteOne)
|
||||||
|
route.Post("/approvals", ctrl.Approval)
|
||||||
|
}
|
||||||
@@ -0,0 +1,727 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
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"
|
||||||
|
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TransferLayingService interface {
|
||||||
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error)
|
||||||
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, error)
|
||||||
|
GetOneWithApproval(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error)
|
||||||
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error)
|
||||||
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error)
|
||||||
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
|
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type transferLayingService struct {
|
||||||
|
Log *logrus.Logger
|
||||||
|
Validate *validator.Validate
|
||||||
|
Repository repository.TransferLayingRepository
|
||||||
|
ProjectFlockRepo ProjectFlockRepository.ProjectflockRepository
|
||||||
|
ProjectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository
|
||||||
|
ProjectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository
|
||||||
|
ProductWarehouseRepo rInventory.ProductWarehouseRepository
|
||||||
|
WarehouseRepo rWarehouse.WarehouseRepository
|
||||||
|
ApprovalService commonSvc.ApprovalService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTransferLayingService(
|
||||||
|
repo repository.TransferLayingRepository,
|
||||||
|
projectFlockRepo ProjectFlockRepository.ProjectflockRepository,
|
||||||
|
projectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository,
|
||||||
|
projectFlockPopulationRepo ProjectFlockRepository.ProjectFlockPopulationRepository,
|
||||||
|
productWarehouseRepo rInventory.ProductWarehouseRepository,
|
||||||
|
warehouseRepo rWarehouse.WarehouseRepository,
|
||||||
|
approvalService commonSvc.ApprovalService,
|
||||||
|
validate *validator.Validate,
|
||||||
|
) TransferLayingService {
|
||||||
|
return &transferLayingService{
|
||||||
|
Log: utils.Log,
|
||||||
|
Validate: validate,
|
||||||
|
Repository: repo,
|
||||||
|
ProjectFlockRepo: projectFlockRepo,
|
||||||
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
|
ProjectFlockPopulationRepo: projectFlockPopulationRepo,
|
||||||
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
|
WarehouseRepo: warehouseRepo,
|
||||||
|
ApprovalService: approvalService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("CreatedUser").
|
||||||
|
Preload("Sources").
|
||||||
|
Preload("Sources.SourceProjectFlockKandang").
|
||||||
|
Preload("Sources.ProductWarehouse").
|
||||||
|
Preload("Sources.ProductWarehouse.Product").
|
||||||
|
Preload("Sources.ProductWarehouse.Warehouse").
|
||||||
|
Preload("Targets").
|
||||||
|
Preload("Targets.TargetProjectFlockKandang").
|
||||||
|
Preload("Targets.ProductWarehouse").
|
||||||
|
Preload("Targets.ProductWarehouse.Product").
|
||||||
|
Preload("Targets.ProductWarehouse.Warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error) {
|
||||||
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
|
transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
|
db = s.withRelations(db)
|
||||||
|
if params.SourceProjectFlockId != 0 {
|
||||||
|
db = db.Where("from_project_flock_id = ?", params.SourceProjectFlockId)
|
||||||
|
}
|
||||||
|
if params.TargetProjectFlockId != 0 {
|
||||||
|
db = db.Where("to_project_flock_id = ?", params.TargetProjectFlockId)
|
||||||
|
}
|
||||||
|
if params.TransferDateFrom != "" {
|
||||||
|
db = db.Where("transfer_date >= ?", params.TransferDateFrom)
|
||||||
|
}
|
||||||
|
if params.TransferDateTo != "" {
|
||||||
|
db = db.Where("transfer_date <= ?", params.TransferDateTo)
|
||||||
|
}
|
||||||
|
if params.TransferNumber != "" {
|
||||||
|
db = db.Where("transfer_number ILIKE ?", "%"+params.TransferNumber+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
sortField := "created_at"
|
||||||
|
if params.Sort != "" {
|
||||||
|
sortField = params.Sort
|
||||||
|
}
|
||||||
|
sortOrder := "DESC"
|
||||||
|
if params.Order == "asc" {
|
||||||
|
sortOrder = "ASC"
|
||||||
|
}
|
||||||
|
db = db.Order(fmt.Sprintf("%s %s", sortField, sortOrder))
|
||||||
|
|
||||||
|
return db
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get transferLayings: %+v", err)
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.ApprovalStatus != "" {
|
||||||
|
var filtered []entity.LayingTransfer
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
|
||||||
|
|
||||||
|
for _, transfer := range transferLayings {
|
||||||
|
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, nil)
|
||||||
|
if err == nil && latestApproval != nil && latestApproval.Action != nil {
|
||||||
|
if string(*latestApproval.Action) == params.ApprovalStatus {
|
||||||
|
filtered = append(filtered, transfer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transferLayings = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
return transferLayings, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTransfer, error) {
|
||||||
|
transferLaying, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed get transferLaying by id: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
|
||||||
|
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, nil)
|
||||||
|
if err == nil && latestApproval != nil {
|
||||||
|
transferLaying.LatestApproval = latestApproval
|
||||||
|
}
|
||||||
|
|
||||||
|
return transferLaying, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s transferLayingService) GetOneWithApproval(c *fiber.Ctx, id uint) (*entity.LayingTransfer, *entity.Approval, error) {
|
||||||
|
transferLaying, err := s.GetOne(c, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transferLaying, transferLaying.LatestApproval, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.SourceProjectFlockId, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Source Project Flock not found")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate source project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.TargetProjectFlockId, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Target Project Flock not found")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate target project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, detail := range req.SourceKandangs {
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Source Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get source project flock kandang")
|
||||||
|
}
|
||||||
|
if pfk.ProjectFlockId != req.SourceProjectFlockId {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d does not belong to source project flock %d", detail.ProjectFlockKandangId, req.SourceProjectFlockId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, detail := range req.TargetKandangs {
|
||||||
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
|
commonSvc.RelationCheck{Name: "Target Project Flock Kandang", ID: &detail.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pfk, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), detail.ProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
|
||||||
|
}
|
||||||
|
if pfk.ProjectFlockId != req.TargetProjectFlockId {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Target kandang %d does not belong to target project flock %d", detail.ProjectFlockKandangId, req.TargetProjectFlockId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transferDate, err := utils.ParseDateString(req.TransferDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format")
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalSourceQty, totalTargetQty float64
|
||||||
|
sourceWarehouseMap := make(map[uint]uint)
|
||||||
|
|
||||||
|
for _, sourceDetail := range req.SourceKandangs {
|
||||||
|
if sourceDetail.Quantity <= 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Source kandang quantity must be greater than 0")
|
||||||
|
}
|
||||||
|
totalSourceQty += sourceDetail.Quantity
|
||||||
|
|
||||||
|
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalPopulation float64
|
||||||
|
var productWarehouseId uint
|
||||||
|
for _, pop := range populations {
|
||||||
|
totalPopulation += pop.TotalQty
|
||||||
|
if productWarehouseId == 0 {
|
||||||
|
productWarehouseId = pop.ProductWarehouseId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalPopulation == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available for transfer", sourceDetail.ProjectFlockKandangId))
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalPopulation < sourceDetail.Quantity {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has insufficient quantity. Available: %.0f, Requested: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity))
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, targetDetail := range req.TargetKandangs {
|
||||||
|
if targetDetail.Quantity <= 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Target kandang quantity must be greater than 0")
|
||||||
|
}
|
||||||
|
totalTargetQty += targetDetail.Quantity
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalSourceQty != totalTargetQty {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total source quantity (%f) must equal total target quantity (%f)", totalSourceQty, totalTargetQty))
|
||||||
|
}
|
||||||
|
|
||||||
|
transferNumber := fmt.Sprintf("TL-%d", time.Now().UnixNano())
|
||||||
|
|
||||||
|
createBody := &entity.LayingTransfer{
|
||||||
|
TransferNumber: transferNumber,
|
||||||
|
Notes: req.Reason,
|
||||||
|
FromProjectFlockId: req.SourceProjectFlockId,
|
||||||
|
ToProjectFlockId: req.TargetProjectFlockId,
|
||||||
|
TransferDate: transferDate,
|
||||||
|
PendingUsageQty: &totalSourceQty,
|
||||||
|
CreatedBy: 1, //todo : harus diambil dari auth
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
if err := s.Repository.WithTx(dbTransaction).CreateOne(c.Context(), createBody, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying record")
|
||||||
|
}
|
||||||
|
|
||||||
|
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||||
|
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
||||||
|
|
||||||
|
for _, sourceDetail := range req.SourceKandangs {
|
||||||
|
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]
|
||||||
|
|
||||||
|
source := entity.LayingTransferSource{
|
||||||
|
LayingTransferId: createBody.Id,
|
||||||
|
SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId,
|
||||||
|
Qty: sourceDetail.Quantity,
|
||||||
|
ProductWarehouseId: &productWarehouseId,
|
||||||
|
}
|
||||||
|
if err := dbTransaction.Create(&source).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.reduceProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, sourceDetail.ProjectFlockKandangId, sourceDetail.Quantity); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reduce project flock population")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"quantity": gorm.Expr("quantity - ?", sourceDetail.Quantity)}, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update source warehouse quantity")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, targetDetail := range req.TargetKandangs {
|
||||||
|
|
||||||
|
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No warehouse found for target kandang %d", targetDetail.ProjectFlockKandangId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
target := entity.LayingTransferTarget{
|
||||||
|
LayingTransferId: createBody.Id,
|
||||||
|
TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
|
||||||
|
Qty: targetDetail.Quantity,
|
||||||
|
ProductWarehouseId: &targetWarehouse.Id,
|
||||||
|
}
|
||||||
|
if err := dbTransaction.Create(&target).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := createApprovalTransferLaying(c.Context(), dbTransaction, createBody.Id, createBody.CreatedBy); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetOne(c, createBody.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.Repository.GetByID(c.Context(), id, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
|
||||||
|
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
||||||
|
}
|
||||||
|
|
||||||
|
if latestApproval != nil && latestApproval.Action != nil {
|
||||||
|
action := string(*latestApproval.Action)
|
||||||
|
if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot update transfer laying with status %s", action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBody := make(map[string]any)
|
||||||
|
|
||||||
|
if req.TransferDate != nil {
|
||||||
|
updateBody["transfer_date"] = *req.TransferDate
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Reason != nil {
|
||||||
|
updateBody["notes"] = *req.Reason
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updateBody) == 0 {
|
||||||
|
return s.GetOne(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to update transferLaying: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetOne(c, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
|
|
||||||
|
_, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Sources.ProductWarehouse").Preload("Targets")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
|
||||||
|
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
||||||
|
}
|
||||||
|
|
||||||
|
if latestApproval != nil && latestApproval.Action != nil {
|
||||||
|
action := string(*latestApproval.Action)
|
||||||
|
if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete transfer laying with status %s", action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||||
|
|
||||||
|
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
||||||
|
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), id)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, source := range sources {
|
||||||
|
if source.ProductWarehouseId != nil && source.Qty > 0 {
|
||||||
|
|
||||||
|
if err := productWarehouseRepoTx.PatchOne(c.Context(), *source.ProductWarehouseId, map[string]any{
|
||||||
|
"quantity": gorm.Expr("quantity + ?", source.Qty),
|
||||||
|
}, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore source warehouse quantity")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
||||||
|
for _, source := range sources {
|
||||||
|
populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations for restoration")
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingToRestore := source.Qty
|
||||||
|
for i := len(populations) - 1; i >= 0 && remainingToRestore > 0; i-- {
|
||||||
|
pop := populations[i]
|
||||||
|
restoreAmount := remainingToRestore
|
||||||
|
if remainingToRestore < pop.TotalQty {
|
||||||
|
|
||||||
|
restoreAmount = remainingToRestore
|
||||||
|
}
|
||||||
|
|
||||||
|
newQty := pop.TotalQty + restoreAmount
|
||||||
|
if err := projectFlockPopulationRepoTx.PatchOne(c.Context(), pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore population quantity")
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingToRestore -= restoreAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Repository.WithTx(dbTransaction).DeleteOne(c.Context(), id); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return fiberErr
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to delete transferLaying: %+v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID := uint(1) // TODO: change from auth context
|
||||||
|
var action entity.ApprovalAction
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
|
||||||
|
case string(entity.ApprovalActionRejected):
|
||||||
|
action = entity.ApprovalActionRejected
|
||||||
|
case string(entity.ApprovalActionApproved):
|
||||||
|
action = entity.ApprovalActionApproved
|
||||||
|
default:
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
|
||||||
|
}
|
||||||
|
|
||||||
|
approvableIDs := utils.UniqueUintSlice(req.ApprovableIds)
|
||||||
|
if len(approvableIDs) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
|
||||||
|
}
|
||||||
|
|
||||||
|
step := utils.TransferToLayingStepPengajuan
|
||||||
|
if action == entity.ApprovalActionApproved {
|
||||||
|
step = utils.TransferToLayingStepDisetujui
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
||||||
|
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
|
||||||
|
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||||
|
|
||||||
|
for _, approvableID := range approvableIDs {
|
||||||
|
transfer, err := s.Repository.GetByID(c.Context(), approvableID, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("TransferLaying %d not found", approvableID))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowTransferToLaying,
|
||||||
|
approvableID,
|
||||||
|
step,
|
||||||
|
&action,
|
||||||
|
actorID,
|
||||||
|
req.Notes,
|
||||||
|
); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == entity.ApprovalActionApproved && transfer.PendingUsageQty != nil && *transfer.PendingUsageQty > 0 {
|
||||||
|
|
||||||
|
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources")
|
||||||
|
}
|
||||||
|
|
||||||
|
targets, err := targetRepoTx.GetByLayingTransferId(c.Context(), approvableID)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer targets")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sources) > 0 && len(targets) > 0 {
|
||||||
|
firstSource := sources[0]
|
||||||
|
if firstSource.ProductWarehouseId == nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID))
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceWarehouse, err := productWarehouseRepoTx.GetByID(c.Context(), *firstSource.ProductWarehouseId, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, target := range targets {
|
||||||
|
|
||||||
|
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), target.TargetProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
|
||||||
|
}
|
||||||
|
|
||||||
|
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.getOrCreateProductWarehouse(
|
||||||
|
c.Context(),
|
||||||
|
dbTransaction,
|
||||||
|
sourceWarehouse.ProductId,
|
||||||
|
targetWarehouse.Id,
|
||||||
|
target.Qty,
|
||||||
|
actorID,
|
||||||
|
); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create or update product warehouse")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usageQty := *transfer.PendingUsageQty
|
||||||
|
updateData := map[string]any{
|
||||||
|
"usage_qty": usageQty,
|
||||||
|
"pending_usage_qty": nil,
|
||||||
|
}
|
||||||
|
if err := s.Repository.WithTx(dbTransaction).PatchOne(c.Context(), approvableID, updateData, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := make([]entity.LayingTransfer, 0, len(approvableIDs))
|
||||||
|
for _, approvableID := range approvableIDs {
|
||||||
|
transfer, err := s.GetOne(c, approvableID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
updated = append(updated, *transfer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayingID uint, actorID uint) error {
|
||||||
|
if transferLayingID == 0 || actorID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||||
|
action := entity.ApprovalActionCreated
|
||||||
|
|
||||||
|
_, err := approvalSvc.CreateApproval(
|
||||||
|
ctx,
|
||||||
|
utils.ApprovalWorkflowTransferToLaying,
|
||||||
|
transferLayingID,
|
||||||
|
utils.TransferToLayingStepPengajuan,
|
||||||
|
&action,
|
||||||
|
actorID,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, tx *gorm.DB, productID uint, warehouseID uint, quantity float64, actorID uint) (*entity.ProductWarehouse, error) {
|
||||||
|
|
||||||
|
productWarehouseRepoTx := rInventory.NewProductWarehouseRepository(tx)
|
||||||
|
|
||||||
|
existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
|
||||||
|
if err == nil && existing != nil {
|
||||||
|
|
||||||
|
if err := productWarehouseRepoTx.PatchOne(ctx, existing.Id, map[string]any{"quantity": gorm.Expr("quantity + ?", quantity)}, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return existing, nil
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
newWarehouse := &entity.ProductWarehouse{
|
||||||
|
ProductId: productID,
|
||||||
|
WarehouseId: warehouseID,
|
||||||
|
Quantity: quantity,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newWarehouse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *transferLayingService) reduceProjectFlockPopulation(ctx context.Context, populationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, projectFlockKandangID uint, quantityToReduce float64) error {
|
||||||
|
|
||||||
|
populations, err := populationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(populations) == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "No populations found for reduction")
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingToReduce := quantityToReduce
|
||||||
|
|
||||||
|
for i := len(populations) - 1; i >= 0; i-- {
|
||||||
|
if remainingToReduce <= 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
pop := populations[i]
|
||||||
|
reductionAmount := remainingToReduce
|
||||||
|
if pop.TotalQty < remainingToReduce {
|
||||||
|
reductionAmount = pop.TotalQty
|
||||||
|
}
|
||||||
|
|
||||||
|
newQty := pop.TotalQty - reductionAmount
|
||||||
|
if err := populationRepo.PatchOne(ctx, pop.Id, map[string]any{"total_qty": newQty}, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingToReduce -= reductionAmount
|
||||||
|
}
|
||||||
|
|
||||||
|
if remainingToReduce > 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient population to reduce. Still need to reduce: %.0f", remainingToReduce))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type SourceKandangDetail struct {
|
||||||
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
|
||||||
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TargetKandangDetail struct {
|
||||||
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required"`
|
||||||
|
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Create struct {
|
||||||
|
TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"`
|
||||||
|
SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"`
|
||||||
|
TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"`
|
||||||
|
SourceKandangs []SourceKandangDetail `json:"source_kandangs" validate:"required,min=1,dive,required"`
|
||||||
|
TargetKandangs []TargetKandangDetail `json:"target_kandangs" validate:"required,min=1,dive,required"`
|
||||||
|
Reason string `json:"reason" validate:"omitempty,max=1000"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Update struct {
|
||||||
|
TransferDate *string `json:"transfer_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||||
|
Reason *string `json:"reason,omitempty" validate:"omitempty,max=1000"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
SourceProjectFlockId uint `query:"source_project_flock_id" validate:"omitempty"`
|
||||||
|
TargetProjectFlockId uint `query:"target_project_flock_id" validate:"omitempty"`
|
||||||
|
TransferDateFrom string `query:"transfer_date_from" validate:"omitempty,datetime=2006-01-02"`
|
||||||
|
TransferDateTo string `query:"transfer_date_to" validate:"omitempty,datetime=2006-01-02"`
|
||||||
|
ApprovalStatus string `query:"approval_status" validate:"omitempty,oneof=PENDING APPROVED REJECTED"` // Filter by latest approval status
|
||||||
|
TransferNumber string `query:"transfer_number" validate:"omitempty"` // Search by transfer number
|
||||||
|
Sort string `query:"sort" validate:"omitempty,oneof=created_at transfer_date"` // Sort by field
|
||||||
|
Order string `query:"order" validate:"omitempty,oneof=asc desc"` // Sort order
|
||||||
|
}
|
||||||
|
|
||||||
|
type Approve struct {
|
||||||
|
Action string `json:"action" validate:"required_strict"`
|
||||||
|
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||||
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/dto"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseController struct {
|
||||||
|
service service.PurchaseService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPurchaseController(s service.PurchaseService) *PurchaseController {
|
||||||
|
return &PurchaseController{service: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error {
|
||||||
|
query := &validation.PurchaseQuery{
|
||||||
|
Page: c.QueryInt("page", 1),
|
||||||
|
Limit: c.QueryInt("limit", 10),
|
||||||
|
Search: strings.TrimSpace(c.Query("search")),
|
||||||
|
PrNumber: strings.TrimSpace(c.Query("pr_number")),
|
||||||
|
CreatedFrom: strings.TrimSpace(c.Query("created_from")),
|
||||||
|
CreatedTo: strings.TrimSpace(c.Query("created_to")),
|
||||||
|
}
|
||||||
|
|
||||||
|
if supplierID := c.QueryInt("supplier_id", 0); supplierID > 0 {
|
||||||
|
query.SupplierID = uint(supplierID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status := strings.TrimSpace(c.Query("status")); status != "" {
|
||||||
|
query.Status = strings.ToUpper(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, total, err := ctrl.service.GetAll(c, query)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := query.Limit
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
page := query.Page
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.SuccessWithPaginate[dto.PurchaseListItemDTO]{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Purchase fetched successfully",
|
||||||
|
Meta: response.Meta{
|
||||||
|
Page: page,
|
||||||
|
Limit: limit,
|
||||||
|
TotalPages: int64(math.Ceil(float64(total) / float64(limit))),
|
||||||
|
TotalResults: total,
|
||||||
|
},
|
||||||
|
Data: dto.ToPurchaseListDTOs(results),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *PurchaseController) GetOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
id, err := strconv.ParseUint(param, 10, 64)
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ctrl.service.GetOne(c, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Purchase fetched successfully",
|
||||||
|
Data: dto.ToPurchaseDetailDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error {
|
||||||
|
req := new(validation.CreatePurchaseRequest)
|
||||||
|
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ctrl.service.CreateOne(c, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusCreated,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Purchase created successfully",
|
||||||
|
Data: dto.ToPurchaseDetailDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
id, err := strconv.ParseUint(param, 10, 64)
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(validation.ApproveStaffPurchaseRequest)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid request body: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ctrl.service.ApproveStaffPurchase(c, id, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Staff purchase approval recorded successfully",
|
||||||
|
Data: dto.ToPurchaseDetailDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (ctrl *PurchaseController) ApproveManagerPurchase(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
id, err := strconv.ParseUint(param, 10, 64)
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(validation.ApproveManagerPurchaseRequest)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ctrl.service.ApproveManagerPurchase(c, id, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Manager purchase approval recorded successfully",
|
||||||
|
Data: dto.ToPurchaseDetailDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
id, err := strconv.ParseUint(param, 10, 64)
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(validation.ReceivePurchaseRequest)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ctrl.service.ReceiveProducts(c, id, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Purchase receiving recorded successfully",
|
||||||
|
Data: dto.ToPurchaseDetailDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *PurchaseController) DeleteItems(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
id, err := strconv.ParseUint(param, 10, 64)
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(validation.DeletePurchaseItemsRequest)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ctrl.service.DeleteItems(c, id, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Purchase items deleted successfully",
|
||||||
|
Data: dto.ToPurchaseDetailDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctrl *PurchaseController) DeletePurchase(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
id, err := strconv.ParseUint(param, 10, 64)
|
||||||
|
if err != nil || id == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctrl.service.DeletePurchase(c, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Purchase deleted successfully",
|
||||||
|
Data: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
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"
|
||||||
|
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||||
|
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
|
||||||
|
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||||
|
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseListItemDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
PrNumber string `json:"pr_number"`
|
||||||
|
PoNumber *string `json:"po_number"`
|
||||||
|
Supplier *supplierDTO.SupplierBaseDTO `json:"supplier"`
|
||||||
|
CreditTerm *int `json:"credit_term"`
|
||||||
|
DueDate *time.Time `json:"due_date"`
|
||||||
|
PoDate *time.Time `json:"po_date"`
|
||||||
|
GrandTotal float64 `json:"grand_total"`
|
||||||
|
Notes *string `json:"notes"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Approval *approvalDTO.ApprovalBaseDTO `json:"approval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseDetailDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
PrNumber string `json:"pr_number"`
|
||||||
|
PoNumber *string `json:"po_number"`
|
||||||
|
Supplier *supplierDTO.SupplierBaseDTO `json:"supplier"`
|
||||||
|
CreditTerm *int `json:"credit_term"`
|
||||||
|
DueDate *time.Time `json:"due_date"`
|
||||||
|
PoDate *time.Time `json:"po_date"`
|
||||||
|
GrandTotal float64 `json:"grand_total"`
|
||||||
|
Notes *string `json:"notes"`
|
||||||
|
Items []PurchaseItemDTO `json:"items"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Approval *approvalDTO.ApprovalBaseDTO `json:"approval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseItemDTO struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
ProductID uint64 `json:"product_id"`
|
||||||
|
Product *productDTO.ProductBaseDTO `json:"product"`
|
||||||
|
WarehouseID uint64 `json:"warehouse_id"`
|
||||||
|
Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse"`
|
||||||
|
ProductWarehouseID *uint64 `json:"product_warehouse_id"`
|
||||||
|
SubQty float64 `json:"sub_qty"`
|
||||||
|
TotalQty float64 `json:"total_qty"`
|
||||||
|
TotalUsed float64 `json:"total_used"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
TotalPrice float64 `json:"total_price"`
|
||||||
|
ReceivedDate *time.Time `json:"received_date"`
|
||||||
|
TravelNumber *string `json:"travel_number"`
|
||||||
|
TravelDocumentPath *string `json:"travel_document_path"`
|
||||||
|
VehicleNumber *string `json:"vehicle_number"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
|
||||||
|
dto := PurchaseItemDTO{
|
||||||
|
Id: item.Id,
|
||||||
|
ProductID: item.ProductId,
|
||||||
|
ProductWarehouseID: item.ProductWarehouseId,
|
||||||
|
WarehouseID: item.WarehouseId,
|
||||||
|
SubQty: item.SubQty,
|
||||||
|
TotalQty: item.TotalQty,
|
||||||
|
TotalUsed: item.TotalUsed,
|
||||||
|
Price: item.Price,
|
||||||
|
TotalPrice: item.TotalPrice,
|
||||||
|
ReceivedDate: item.ReceivedDate,
|
||||||
|
TravelNumber: item.TravelNumber,
|
||||||
|
TravelDocumentPath: item.TravelNumberDocs,
|
||||||
|
VehicleNumber: item.VehicleNumber,
|
||||||
|
}
|
||||||
|
if item.Product != nil && item.Product.Id != 0 {
|
||||||
|
summary := productDTO.ToProductBaseDTO(*item.Product)
|
||||||
|
dto.Product = &summary
|
||||||
|
}
|
||||||
|
if item.Warehouse != nil && item.Warehouse.Id != 0 {
|
||||||
|
summary := warehouseDTO.ToWarehouseBaseDTO(*item.Warehouse)
|
||||||
|
if item.Warehouse.Area.Id != 0 {
|
||||||
|
areaSummary := areaDTO.ToAreaBaseDTO(item.Warehouse.Area)
|
||||||
|
summary.Area = &areaSummary
|
||||||
|
}
|
||||||
|
if item.Warehouse.Location != nil && item.Warehouse.Location.Id != 0 {
|
||||||
|
locationSummary := locationDTO.ToLocationBaseDTO(*item.Warehouse.Location)
|
||||||
|
summary.Location = &locationSummary
|
||||||
|
}
|
||||||
|
dto.Warehouse = &summary
|
||||||
|
}
|
||||||
|
return dto
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPurchaseItemDTOs(items []entity.PurchaseItem) []PurchaseItemDTO {
|
||||||
|
result := make([]PurchaseItemDTO, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
result[i] = ToPurchaseItemDTO(item)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPurchaseDetailDTO(p entity.Purchase) PurchaseDetailDTO {
|
||||||
|
dto := PurchaseDetailDTO{
|
||||||
|
Id: p.Id,
|
||||||
|
PrNumber: p.PrNumber,
|
||||||
|
PoNumber: p.PoNumber,
|
||||||
|
Supplier: mapSupplier(p.Supplier),
|
||||||
|
CreditTerm: p.CreditTerm,
|
||||||
|
DueDate: p.DueDate,
|
||||||
|
PoDate: p.PoDate,
|
||||||
|
GrandTotal: p.GrandTotal,
|
||||||
|
Notes: p.Notes,
|
||||||
|
Items: ToPurchaseItemDTOs(p.Items),
|
||||||
|
CreatedAt: p.CreatedAt,
|
||||||
|
UpdatedAt: p.UpdatedAt,
|
||||||
|
}
|
||||||
|
if approval := toPurchaseApprovalDTO(p); approval != nil {
|
||||||
|
dto.Approval = approval
|
||||||
|
}
|
||||||
|
return dto
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPurchaseListDTO(p entity.Purchase) PurchaseListItemDTO {
|
||||||
|
dto := PurchaseListItemDTO{
|
||||||
|
Id: p.Id,
|
||||||
|
PrNumber: p.PrNumber,
|
||||||
|
PoNumber: p.PoNumber,
|
||||||
|
Supplier: mapSupplier(p.Supplier),
|
||||||
|
CreditTerm: p.CreditTerm,
|
||||||
|
DueDate: p.DueDate,
|
||||||
|
PoDate: p.PoDate,
|
||||||
|
GrandTotal: p.GrandTotal,
|
||||||
|
Notes: p.Notes,
|
||||||
|
CreatedAt: p.CreatedAt,
|
||||||
|
UpdatedAt: p.UpdatedAt,
|
||||||
|
}
|
||||||
|
if approval := toPurchaseApprovalDTO(p); approval != nil {
|
||||||
|
dto.Approval = approval
|
||||||
|
}
|
||||||
|
return dto
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapSupplier(s entity.Supplier) *supplierDTO.SupplierBaseDTO {
|
||||||
|
if s.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
summary := supplierDTO.ToSupplierBaseDTO(s)
|
||||||
|
return &summary
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPurchaseListDTOs(items []entity.Purchase) []PurchaseListItemDTO {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := make([]PurchaseListItemDTO, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
result[i] = ToPurchaseListDTO(item)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPurchaseApprovalDTO(p entity.Purchase) *approvalDTO.ApprovalBaseDTO {
|
||||||
|
if p.LatestApproval == nil || p.LatestApproval.Id == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*p.LatestApproval)
|
||||||
|
return &mapped
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package purchases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
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"
|
||||||
|
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
|
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||||
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
|
||||||
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseModule struct{}
|
||||||
|
|
||||||
|
func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
|
purchaseRepo := rPurchase.NewPurchaseRepository(db)
|
||||||
|
productRepo := rProduct.NewProductRepository(db)
|
||||||
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
|
supplierRepo := rSupplier.NewSupplierRepository(db)
|
||||||
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
|
||||||
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPurchase, utils.PurchaseApprovalSteps); err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to register purchase approval workflow: %v", err))
|
||||||
|
}
|
||||||
|
expenseBridge := service.NewNoopPurchaseExpenseBridge()
|
||||||
|
|
||||||
|
purchaseService := service.NewPurchaseService(
|
||||||
|
validate,
|
||||||
|
purchaseRepo,
|
||||||
|
productRepo,
|
||||||
|
warehouseRepo,
|
||||||
|
supplierRepo,
|
||||||
|
productWarehouseRepo,
|
||||||
|
approvalRepo,
|
||||||
|
approvalService,
|
||||||
|
expenseBridge,
|
||||||
|
)
|
||||||
|
|
||||||
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
Routes(router, purchaseService, userService)
|
||||||
|
}
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
package repositories
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PurchaseRepository interface {
|
||||||
|
repository.BaseRepository[entity.Purchase]
|
||||||
|
CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error
|
||||||
|
CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error
|
||||||
|
GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error)
|
||||||
|
GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error)
|
||||||
|
UpdatePricing(ctx context.Context, purchaseID uint64, updates []PurchasePricingUpdate, grandTotal float64) error
|
||||||
|
UpdateReceivingDetails(ctx context.Context, purchaseID uint64, updates []PurchaseReceivingUpdate) error
|
||||||
|
DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error
|
||||||
|
WithListRelations() func(*gorm.DB) *gorm.DB
|
||||||
|
UpdateGrandTotal(ctx context.Context, purchaseID uint64, grandTotal float64) error
|
||||||
|
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||||
|
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseRepositoryImpl struct {
|
||||||
|
*repository.BaseRepositoryImpl[entity.Purchase]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPurchaseRepository(db *gorm.DB) PurchaseRepository {
|
||||||
|
return &PurchaseRepositoryImpl{
|
||||||
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.Purchase](db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseListFilter struct {
|
||||||
|
SupplierID uint
|
||||||
|
Search string
|
||||||
|
PrNumber string
|
||||||
|
CreatedFrom *time.Time
|
||||||
|
CreatedTo *time.Time
|
||||||
|
Status *entity.ApprovalAction
|
||||||
|
CompletedOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error {
|
||||||
|
db := r.DB().WithContext(ctx)
|
||||||
|
|
||||||
|
if err := db.Create(purchase).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
item.PurchaseId = purchase.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&items).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error {
|
||||||
|
if len(items) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
if item == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item.PurchaseId = purchaseID
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.DB().WithContext(ctx).Create(&items).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) {
|
||||||
|
var purchase entity.Purchase
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Scopes(r.withDetailRelations).
|
||||||
|
First(&purchase, id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &purchase, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error) {
|
||||||
|
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
|
||||||
|
db = r.withListRelations(db)
|
||||||
|
return r.applyListFilters(db, filter)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) WithListRelations() func(*gorm.DB) *gorm.DB {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return r.withListRelations(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) withDetailRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("Supplier").
|
||||||
|
Preload("Items", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Order("id ASC")
|
||||||
|
}).
|
||||||
|
Preload("Items.Product").
|
||||||
|
Preload("Items.Warehouse").
|
||||||
|
Preload("Items.Warehouse.Area").
|
||||||
|
Preload("Items.Warehouse.Location").
|
||||||
|
Preload("Items.ProductWarehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) WithDetailRelations() func(*gorm.DB) *gorm.DB {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return r.withDetailRelations(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchasePricingUpdate struct {
|
||||||
|
ItemID uint64
|
||||||
|
ProductID *uint64
|
||||||
|
Price float64
|
||||||
|
TotalPrice float64
|
||||||
|
Quantity *float64
|
||||||
|
TotalQty *float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseReceivingUpdate struct {
|
||||||
|
ItemID uint64
|
||||||
|
ReceivedDate *time.Time
|
||||||
|
TravelNumber *string
|
||||||
|
TravelDocumentPath *string
|
||||||
|
VehicleNumber *string
|
||||||
|
ReceivedQty *float64
|
||||||
|
WarehouseID *uint
|
||||||
|
ProductWarehouseID *uint
|
||||||
|
ClearProductWarehouse bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) UpdatePricing(
|
||||||
|
ctx context.Context,
|
||||||
|
purchaseID uint64,
|
||||||
|
updates []PurchasePricingUpdate,
|
||||||
|
grandTotal float64,
|
||||||
|
) error {
|
||||||
|
if len(updates) == 0 {
|
||||||
|
return errors.New("pricing updates cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx)
|
||||||
|
|
||||||
|
for _, upd := range updates {
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"price": upd.Price,
|
||||||
|
"total_price": upd.TotalPrice,
|
||||||
|
}
|
||||||
|
if upd.ProductID != nil {
|
||||||
|
data["product_id"] = *upd.ProductID
|
||||||
|
}
|
||||||
|
if upd.Quantity != nil {
|
||||||
|
data["sub_qty"] = *upd.Quantity
|
||||||
|
}
|
||||||
|
if upd.TotalQty != nil {
|
||||||
|
data["total_qty"] = *upd.TotalQty
|
||||||
|
}
|
||||||
|
|
||||||
|
result := db.Model(&entity.PurchaseItem{}).
|
||||||
|
Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
|
||||||
|
Updates(data)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Model(&entity.Purchase{}).
|
||||||
|
Where("id = ?", purchaseID).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"grand_total": grandTotal,
|
||||||
|
"updated_at": gorm.Expr("NOW()"),
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
|
||||||
|
ctx context.Context,
|
||||||
|
purchaseID uint64,
|
||||||
|
updates []PurchaseReceivingUpdate,
|
||||||
|
) error {
|
||||||
|
if len(updates) == 0 {
|
||||||
|
return errors.New("receiving updates cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := r.DB().WithContext(ctx)
|
||||||
|
|
||||||
|
for _, upd := range updates {
|
||||||
|
data := map[string]interface{}{}
|
||||||
|
|
||||||
|
if upd.ReceivedDate != nil {
|
||||||
|
data["received_date"] = upd.ReceivedDate
|
||||||
|
}
|
||||||
|
if upd.TravelNumber != nil {
|
||||||
|
data["travel_number"] = upd.TravelNumber
|
||||||
|
}
|
||||||
|
if upd.TravelDocumentPath != nil {
|
||||||
|
data["travel_number_docs"] = upd.TravelDocumentPath
|
||||||
|
}
|
||||||
|
if upd.VehicleNumber != nil {
|
||||||
|
data["vehicle_number"] = upd.VehicleNumber
|
||||||
|
}
|
||||||
|
if upd.ReceivedQty != nil {
|
||||||
|
data["total_qty"] = upd.ReceivedQty
|
||||||
|
}
|
||||||
|
if upd.WarehouseID != nil && *upd.WarehouseID != 0 {
|
||||||
|
data["warehouse_id"] = upd.WarehouseID
|
||||||
|
}
|
||||||
|
|
||||||
|
if upd.ProductWarehouseID != nil {
|
||||||
|
data["product_warehouse_id"] = *upd.ProductWarehouseID
|
||||||
|
} else if upd.ClearProductWarehouse {
|
||||||
|
data["product_warehouse_id"] = gorm.Expr("NULL")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result := db.Model(&entity.PurchaseItem{}).
|
||||||
|
Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
|
||||||
|
Updates(data)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) UpdateGrandTotal(
|
||||||
|
ctx context.Context,
|
||||||
|
purchaseID uint64,
|
||||||
|
grandTotal float64,
|
||||||
|
) error {
|
||||||
|
return r.DB().WithContext(ctx).
|
||||||
|
Model(&entity.Purchase{}).
|
||||||
|
Where("id = ?", purchaseID).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"grand_total": grandTotal,
|
||||||
|
"updated_at": gorm.Expr("NOW()"),
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error {
|
||||||
|
if len(itemIDs) == 0 {
|
||||||
|
return errors.New("itemIDs cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.DB().WithContext(ctx).
|
||||||
|
Where("purchase_id = ? AND id IN ?", purchaseID, itemIDs).
|
||||||
|
Delete(&entity.PurchaseItem{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) {
|
||||||
|
return r.generateSequentialNumber(ctx, tx, "pr_number", utils.PurchasePRNumberPrefix, utils.PurchaseNumberPadding)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
|
||||||
|
return r.generateSequentialNumber(ctx, tx, "po_number", utils.PurchasePONumberPrefix, utils.PurchaseNumberPadding)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) generateSequentialNumber(ctx context.Context, tx *gorm.DB, column, prefix string, padding int) (string, error) {
|
||||||
|
db := tx
|
||||||
|
if db == nil {
|
||||||
|
db = r.DB()
|
||||||
|
}
|
||||||
|
|
||||||
|
var values []string
|
||||||
|
err := db.WithContext(ctx).
|
||||||
|
Model(&entity.Purchase{}).
|
||||||
|
Where(fmt.Sprintf("%s LIKE ?", column), prefix+"%").
|
||||||
|
Select(column).
|
||||||
|
Order(fmt.Sprintf("%s DESC", column)).
|
||||||
|
Limit(20).
|
||||||
|
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
|
Pluck(column, &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, padding, next)
|
||||||
|
exists, err := r.numberExists(ctx, db, column, candidate)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return candidate, nil
|
||||||
|
}
|
||||||
|
next++
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("unable to generate unique %s", column)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, column, value string) (bool, error) {
|
||||||
|
var count int64
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Model(&entity.Purchase{}).
|
||||||
|
Where(fmt.Sprintf("%s = ?", column), 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) withListRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Supplier")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) applyListFilters(db *gorm.DB, filter *PurchaseListFilter) *gorm.DB {
|
||||||
|
if filter == nil {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.SupplierID > 0 {
|
||||||
|
db = db.Where("purchases.supplier_id = ?", filter.SupplierID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if search := strings.ToLower(strings.TrimSpace(filter.Search)); search != "" {
|
||||||
|
like := "%" + search + "%"
|
||||||
|
db = db.Where("(LOWER(purchases.pr_number) LIKE ? OR LOWER(COALESCE(purchases.notes, '')) LIKE ?)", like, like)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pr := strings.TrimSpace(filter.PrNumber); pr != "" {
|
||||||
|
db = db.Where("purchases.pr_number ILIKE ?", "%"+pr+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.CreatedFrom != nil {
|
||||||
|
db = db.Where("purchases.created_at >= ?", *filter.CreatedFrom)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.CreatedTo != nil {
|
||||||
|
db = db.Where("purchases.created_at < ?", *filter.CreatedTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.CompletedOnly {
|
||||||
|
step := uint16(utils.PurchaseStepCompleted)
|
||||||
|
db = r.applyLatestApprovalFilter(db, entity.ApprovalActionApproved, &step)
|
||||||
|
} else if filter.Status != nil {
|
||||||
|
db = r.applyLatestApprovalFilter(db, *filter.Status, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Order("purchases.created_at DESC").Order("purchases.id DESC")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PurchaseRepositoryImpl) applyLatestApprovalFilter(db *gorm.DB, action entity.ApprovalAction, minStep *uint16) *gorm.DB {
|
||||||
|
latestSub := r.DB().
|
||||||
|
Model(&entity.Approval{}).
|
||||||
|
Select("approvable_id, MAX(action_at) AS latest_action_at").
|
||||||
|
Where("approvable_type = ?", utils.ApprovalWorkflowPurchase.String()).
|
||||||
|
Group("approvable_id")
|
||||||
|
|
||||||
|
db = db.
|
||||||
|
Joins("LEFT JOIN (?) AS latest_purchase_approvals ON latest_purchase_approvals.approvable_id = purchases.id", latestSub).
|
||||||
|
Joins(
|
||||||
|
"LEFT JOIN approvals ON approvals.approvable_id = purchases.id AND approvals.approvable_type = ? AND approvals.action_at = latest_purchase_approvals.latest_action_at",
|
||||||
|
utils.ApprovalWorkflowPurchase.String(),
|
||||||
|
).
|
||||||
|
Where("approvals.action = ?", string(action))
|
||||||
|
if minStep != nil {
|
||||||
|
db = db.Where("approvals.step_number >= ?", *minStep)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package purchases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
|
||||||
|
middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/controllers"
|
||||||
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
|
||||||
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Routes(router fiber.Router, purchaseService service.PurchaseService, userService user.UserService) {
|
||||||
|
ctrl := controller.NewPurchaseController(purchaseService)
|
||||||
|
|
||||||
|
route := router.Group("/purchases")
|
||||||
|
route.Use(middleware.Auth(userService))
|
||||||
|
|
||||||
|
route.Get("/", ctrl.GetAll)
|
||||||
|
route.Get("/:id", ctrl.GetOne)
|
||||||
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
route.Post("/:id/approvals/staff", ctrl.ApproveStaffPurchase)
|
||||||
|
route.Post("/:id/approvals/manager", ctrl.ApproveManagerPurchase)
|
||||||
|
route.Post("/:id/receipts", ctrl.ReceiveProducts)
|
||||||
|
route.Delete("/:id", ctrl.DeletePurchase)
|
||||||
|
route.Delete("/:id/items", ctrl.DeleteItems)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PurchaseExpenseBridge defines hooks that allow purchase flows to stay in sync with expense data once it exists.
|
||||||
|
type PurchaseExpenseBridge interface {
|
||||||
|
OnItemsCreated(ctx context.Context, purchaseID uint64, items []entity.PurchaseItem) error
|
||||||
|
OnItemsDeleted(ctx context.Context, purchaseID uint64, itemIDs []uint64) error
|
||||||
|
OnItemsReceived(ctx context.Context, purchaseID uint64, updates []ExpenseReceivingPayload) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpenseReceivingPayload captures the minimum data expense integration will need once available.
|
||||||
|
type ExpenseReceivingPayload struct {
|
||||||
|
PurchaseItemID uint64
|
||||||
|
ProductID uint64
|
||||||
|
WarehouseID uint64
|
||||||
|
ReceivedQty float64
|
||||||
|
ReceivedDate *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// noopPurchaseExpenseBridge is the default implementation until the expense module is ready.
|
||||||
|
type noopPurchaseExpenseBridge struct{}
|
||||||
|
|
||||||
|
func NewNoopPurchaseExpenseBridge() PurchaseExpenseBridge {
|
||||||
|
return &noopPurchaseExpenseBridge{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopPurchaseExpenseBridge) OnItemsCreated(_ context.Context, _ uint64, _ []entity.PurchaseItem) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopPurchaseExpenseBridge) OnItemsDeleted(_ context.Context, _ uint64, _ []uint64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *noopPurchaseExpenseBridge) OnItemsReceived(_ context.Context, _ uint64, _ []ExpenseReceivingPayload) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
|||||||
|
package validation
|
||||||
|
|
||||||
|
type PurchaseItemPayload struct {
|
||||||
|
WarehouseID uint `json:"warehouse_id" validate:"required,gt=0"`
|
||||||
|
ProductID uint `json:"product_id" validate:"required,gt=0"`
|
||||||
|
Quantity float64 `json:"qty" validate:"required,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePurchaseRequest struct {
|
||||||
|
SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
|
||||||
|
CreditTerm int `json:"credit_term" validate:"required,gte=0"`
|
||||||
|
Notes *string `json:"notes" validate:"omitempty,max=500"`
|
||||||
|
Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StaffPurchaseApprovalItem struct {
|
||||||
|
PurchaseItemID uint64 `json:"purchase_item_id,omitempty" validate:"omitempty,gt=0"`
|
||||||
|
// For new items (no purchase_item_id), product_id is required.
|
||||||
|
ProductID uint64 `json:"product_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
|
||||||
|
WarehouseID uint64 `json:"warehouse_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
|
||||||
|
Qty *float64 `json:"qty,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
|
||||||
|
Price float64 `json:"price" validate:"required,gt=0"`
|
||||||
|
TotalPrice float64 `json:"total_price" validate:"required,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApproveStaffPurchaseRequest struct {
|
||||||
|
Items []StaffPurchaseApprovalItem `json:"items" validate:"required,min=1,dive"`
|
||||||
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApproveManagerPurchaseRequest struct {
|
||||||
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReceivePurchaseItemRequest struct {
|
||||||
|
PurchaseItemID uint64 `json:"purchase_item_id" validate:"required,gt=0"`
|
||||||
|
WarehouseID *uint `json:"warehouse_id" validate:"omitempty,gt=0"`
|
||||||
|
ReceivedDate string `json:"received_date" validate:"required,datetime=2006-01-02"`
|
||||||
|
TravelNumber *string `json:"travel_number" validate:"omitempty,max=100"`
|
||||||
|
TravelDocumentPath *string `json:"travel_document_path" validate:"omitempty,max=255"`
|
||||||
|
VehicleNumber *string `json:"vehicle_number" validate:"omitempty,max=100"`
|
||||||
|
ReceivedQty *float64 `json:"received_qty" validate:"omitempty,gte=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReceivePurchaseRequest struct {
|
||||||
|
Items []ReceivePurchaseItemRequest `json:"items" validate:"required,min=1,dive"`
|
||||||
|
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeletePurchaseItemsRequest struct {
|
||||||
|
ItemIDs []uint64 `json:"item_ids" validate:"required,min=1,dive,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PurchaseQuery struct {
|
||||||
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
|
SupplierID uint `query:"supplier_id" validate:"omitempty,gt=0"`
|
||||||
|
Search string `query:"search" validate:"omitempty,max=100"`
|
||||||
|
PrNumber string `query:"pr_number" validate:"omitempty,max=50"`
|
||||||
|
CreatedFrom string `query:"created_from" validate:"omitempty,datetime=2006-01-02"`
|
||||||
|
CreatedTo string `query:"created_to" validate:"omitempty,datetime=2006-01-02"`
|
||||||
|
Status string `query:"status" validate:"omitempty,oneof=CREATED UPDATED APPROVED REJECTED COMPLETED"`
|
||||||
|
}
|
||||||
@@ -12,8 +12,9 @@ import (
|
|||||||
constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants"
|
constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants"
|
||||||
inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory"
|
inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory"
|
||||||
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
|
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
|
||||||
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
|
|
||||||
ssoModule "gitlab.com/mbugroup/lti-api.git/internal/modules/sso"
|
ssoModule "gitlab.com/mbugroup/lti-api.git/internal/modules/sso"
|
||||||
|
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
|
||||||
|
purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases"
|
||||||
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
|
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
|
||||||
// MODULE IMPORTS
|
// MODULE IMPORTS
|
||||||
)
|
)
|
||||||
@@ -29,6 +30,7 @@ func Routes(app *fiber.App, db *gorm.DB) {
|
|||||||
master.MasterModule{},
|
master.MasterModule{},
|
||||||
constants.ConstantModule{},
|
constants.ConstantModule{},
|
||||||
inventory.InventoryModule{},
|
inventory.InventoryModule{},
|
||||||
|
purchases.PurchaseModule{},
|
||||||
production.ProductionModule{},
|
production.ProductionModule{},
|
||||||
approvals.ApprovalModule{},
|
approvals.ApprovalModule{},
|
||||||
ssoModule.Module{},
|
ssoModule.Module{},
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ const (
|
|||||||
FlagIsActive FlagType = "IS_ACTIVE"
|
FlagIsActive FlagType = "IS_ACTIVE"
|
||||||
|
|
||||||
FlagDOC FlagType = "DOC"
|
FlagDOC FlagType = "DOC"
|
||||||
|
FlagPullet FlagType = "PULLET"
|
||||||
|
FlagLayer FlagType = "LAYER"
|
||||||
FlagPakan FlagType = "PAKAN"
|
FlagPakan FlagType = "PAKAN"
|
||||||
FlagPreStarter FlagType = "PRE-STARTER"
|
FlagPreStarter FlagType = "PRE-STARTER"
|
||||||
FlagStarter FlagType = "STARTER"
|
FlagStarter FlagType = "STARTER"
|
||||||
@@ -37,6 +39,8 @@ const (
|
|||||||
var flagGroupOptions = map[FlagGroup][]FlagType{
|
var flagGroupOptions = map[FlagGroup][]FlagType{
|
||||||
FlagGroupProduct: {
|
FlagGroupProduct: {
|
||||||
FlagDOC,
|
FlagDOC,
|
||||||
|
FlagPullet,
|
||||||
|
FlagLayer,
|
||||||
FlagPakan,
|
FlagPakan,
|
||||||
FlagPreStarter,
|
FlagPreStarter,
|
||||||
FlagStarter,
|
FlagStarter,
|
||||||
@@ -79,6 +83,24 @@ const (
|
|||||||
WarehouseTypeKandang WarehouseType = "KANDANG"
|
WarehouseTypeKandang WarehouseType = "KANDANG"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Stock log
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
type StockLogTransactionType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StockLogTransactionTypeIncrease StockLogTransactionType = "INCREASE"
|
||||||
|
StockLogTransactionTypeDecrease StockLogTransactionType = "DECREASE"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StockLogType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StockLogTypeAdjustment StockLogType = "ADJUSTMENT"
|
||||||
|
StockLogTypeTransfer StockLogType = "TRANSFER"
|
||||||
|
)
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// WarehouseType
|
// WarehouseType
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
@@ -140,6 +162,36 @@ var ProjectFlockApprovalSteps = map[approvalutils.ApprovalStep]string{
|
|||||||
ProjectFlockStepAktif: "Aktif",
|
ProjectFlockStepAktif: "Aktif",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Project Flock Kandang Approval
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
const (
|
||||||
|
ApprovalWorkflowProjectFlockKandang approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PROJECT_FLOCK_KANDANGS")
|
||||||
|
ProjectFlockKandangStepPengajuan approvalutils.ApprovalStep = 1
|
||||||
|
ProjectFlockKandangStepDisetujui approvalutils.ApprovalStep = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var ProjectFlockKandangApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||||
|
ProjectFlockKandangStepPengajuan: "Pengajuan",
|
||||||
|
ProjectFlockKandangStepDisetujui: "Disetujui",
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Transfer To laying Approval
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
const (
|
||||||
|
ApprovalWorkflowTransferToLaying approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("TRANSFER_TO_LAYINGS")
|
||||||
|
TransferToLayingStepPengajuan approvalutils.ApprovalStep = 1
|
||||||
|
TransferToLayingStepDisetujui approvalutils.ApprovalStep = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var TransferToLayingApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||||
|
TransferToLayingStepPengajuan: "Pengajuan",
|
||||||
|
TransferToLayingStepDisetujui: "Disetujui",
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// Recording Approval
|
// Recording Approval
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
@@ -153,8 +205,33 @@ const (
|
|||||||
|
|
||||||
var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{
|
var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||||
RecordingStepGradingTelur: "Grading-Telur",
|
RecordingStepGradingTelur: "Grading-Telur",
|
||||||
RecordingStepPengajuan: "Pengajuan",
|
RecordingStepPengajuan: "Pengajuan",
|
||||||
RecordingStepDisetujui: "Disetujui",
|
RecordingStepDisetujui: "Disetujui",
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Purchase Approval
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
|
const (
|
||||||
|
ApprovalWorkflowPurchase approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PURCHASES")
|
||||||
|
PurchaseStepPengajuan approvalutils.ApprovalStep = 1
|
||||||
|
PurchaseStepStaffPurchase approvalutils.ApprovalStep = 2
|
||||||
|
PurchaseStepManager approvalutils.ApprovalStep = 3
|
||||||
|
PurchaseStepReceiving approvalutils.ApprovalStep = 4
|
||||||
|
PurchaseStepCompleted approvalutils.ApprovalStep = 5
|
||||||
|
|
||||||
|
PurchasePRNumberPrefix = "PR-LTI-"
|
||||||
|
PurchasePONumberPrefix = "PO-LTI-"
|
||||||
|
PurchaseNumberPadding = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||||
|
PurchaseStepPengajuan: "Pengajuan",
|
||||||
|
PurchaseStepStaffPurchase: "Staff Purchase",
|
||||||
|
PurchaseStepManager: "Manager Purchase",
|
||||||
|
PurchaseStepReceiving: "Penerimaan Produk",
|
||||||
|
PurchaseStepCompleted: "Selesai",
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user