mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 23:35:43 +00:00
Merge branch 'fix/BE-Document_s3' into 'development'
feat(BE): fix fifo system recording and uniformity dto See merge request mbugroup/lti-api!129
This commit is contained in:
@@ -187,10 +187,11 @@ func (r *BaseRepositoryImpl[T]) PatchOne(
|
|||||||
updates map[string]any,
|
updates map[string]any,
|
||||||
modifier func(*gorm.DB) *gorm.DB,
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
) error {
|
) error {
|
||||||
q := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id)
|
q := r.db.WithContext(ctx)
|
||||||
if modifier != nil {
|
if modifier != nil {
|
||||||
q = modifier(q)
|
q = modifier(q)
|
||||||
}
|
}
|
||||||
|
q = q.Model(new(T)).Where("id = ?", id)
|
||||||
|
|
||||||
result := q.Updates(updates)
|
result := q.Updates(updates)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"mime"
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -305,6 +306,56 @@ func (s *documentService) PresignURL(ctx context.Context, document entity.Docume
|
|||||||
return s.storage.PresignURL(ctx, document.Path, expires)
|
return s.storage.PresignURL(ctx, document.Path, expires)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveDocumentURL normalizes a stored path or URL into a presigned URL.
|
||||||
|
func ResolveDocumentURL(
|
||||||
|
ctx context.Context,
|
||||||
|
svc DocumentService,
|
||||||
|
rawPath string,
|
||||||
|
expires time.Duration,
|
||||||
|
) (string, error) {
|
||||||
|
if svc == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rawPath = strings.TrimSpace(rawPath)
|
||||||
|
if rawPath == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
key := rawPath
|
||||||
|
lower := strings.ToLower(rawPath)
|
||||||
|
if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") {
|
||||||
|
key = extractS3KeyFromURL(rawPath)
|
||||||
|
if key == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc.PresignURL(ctx, entity.Document{Path: key}, expires)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractS3KeyFromURL(raw string) string {
|
||||||
|
parsed, err := url.Parse(strings.TrimSpace(raw))
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
path := strings.TrimPrefix(parsed.Path, "/")
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
host := strings.ToLower(strings.TrimSpace(parsed.Host))
|
||||||
|
if strings.HasPrefix(host, "s3.") || strings.HasPrefix(host, "s3-") {
|
||||||
|
parts := strings.SplitN(path, "/", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
return parts[1]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
func (s *documentService) generateObjectKey(ext string) (string, error) {
|
func (s *documentService) generateObjectKey(ext string) (string, error) {
|
||||||
normalizedExt := strings.TrimSpace(ext)
|
normalizedExt := strings.TrimSpace(ext)
|
||||||
if normalizedExt != "" && !strings.HasPrefix(normalizedExt, ".") {
|
if normalizedExt != "" && !strings.HasPrefix(normalizedExt, ".") {
|
||||||
|
|||||||
@@ -192,7 +192,6 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
|
|||||||
if req.Quantity < 0 {
|
if req.Quantity < 0 {
|
||||||
return nil, errors.New("quantity must be zero or greater")
|
return nil, errors.New("quantity must be zero or greater")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, ok := fifo.Usable(req.UsableKey)
|
cfg, ok := fifo.Usable(req.UsableKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("usable %q is not registered", req.UsableKey)
|
return nil, fmt.Errorf("usable %q is not registered", req.UsableKey)
|
||||||
@@ -220,7 +219,6 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
|
|||||||
currentPending := ctxRow.PendingQty
|
currentPending := ctxRow.PendingQty
|
||||||
currentTotal := currentUsage + currentPending
|
currentTotal := currentUsage + currentPending
|
||||||
delta := req.Quantity - currentTotal
|
delta := req.Quantity - currentTotal
|
||||||
|
|
||||||
var (
|
var (
|
||||||
usageDelta float64
|
usageDelta float64
|
||||||
pendingDelta float64
|
pendingDelta float64
|
||||||
@@ -285,7 +283,6 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
|
|||||||
result.ReleasedQuantity = releasedAmount
|
result.ReleasedQuantity = releasedAmount
|
||||||
result.UsageQuantity = currentUsage + usageDelta
|
result.UsageQuantity = currentUsage + usageDelta
|
||||||
result.PendingQuantity = currentPending + pendingDelta
|
result.PendingQuantity = currentPending + pendingDelta
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -299,7 +296,6 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest)
|
|||||||
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
|
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
|
||||||
return errors.New("usable key and id are required")
|
return errors.New("usable key and id are required")
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
||||||
cfg, ok := fifo.Usable(req.UsableKey)
|
cfg, ok := fifo.Usable(req.UsableKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -310,7 +306,6 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var usageDelta, pendingDelta float64
|
var usageDelta, pendingDelta float64
|
||||||
if ctxRow.UsageQty > 0 {
|
if ctxRow.UsageQty > 0 {
|
||||||
if _, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, ctxRow.UsageQty); err != nil {
|
if _, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, ctxRow.UsageQty); err != nil {
|
||||||
@@ -715,7 +710,7 @@ func (s *fifoService) releaseUsagePortion(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{
|
if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{
|
||||||
"quantity": allocation.Qty - releaseAmt,
|
"qty": allocation.Qty - releaseAmt,
|
||||||
}, func(db *gorm.DB) *gorm.DB {
|
}, func(db *gorm.DB) *gorm.DB {
|
||||||
return s.txOrDB(tx, db)
|
return s.txOrDB(tx, db)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
CREATE OR REPLACE FUNCTION soft_delete_handle_fk() RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
fk record;
|
||||||
|
child_column text;
|
||||||
|
parent_column text;
|
||||||
|
parent_value text;
|
||||||
|
child_has_deleted_at boolean;
|
||||||
|
ref_exists boolean;
|
||||||
|
sql text;
|
||||||
|
BEGIN
|
||||||
|
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
|
||||||
|
FOR fk IN
|
||||||
|
SELECT conrelid::regclass AS child_table,
|
||||||
|
conkey AS child_cols,
|
||||||
|
confkey AS parent_cols,
|
||||||
|
confdeltype
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE contype = 'f'
|
||||||
|
AND confrelid = TG_RELID
|
||||||
|
LOOP
|
||||||
|
IF array_length(fk.child_cols, 1) IS DISTINCT FROM 1
|
||||||
|
OR array_length(fk.parent_cols, 1) IS DISTINCT FROM 1 THEN
|
||||||
|
RAISE NOTICE 'soft_delete_handle_fk skipped composite fk on %', fk.child_table;
|
||||||
|
CONTINUE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT attname INTO child_column
|
||||||
|
FROM pg_attribute
|
||||||
|
WHERE attrelid = fk.child_table
|
||||||
|
AND attnum = fk.child_cols[1]
|
||||||
|
AND NOT attisdropped;
|
||||||
|
|
||||||
|
SELECT attname INTO parent_column
|
||||||
|
FROM pg_attribute
|
||||||
|
WHERE attrelid = TG_RELID
|
||||||
|
AND attnum = fk.parent_cols[1]
|
||||||
|
AND NOT attisdropped;
|
||||||
|
|
||||||
|
EXECUTE format('SELECT ($1).%I', parent_column)
|
||||||
|
INTO parent_value
|
||||||
|
USING OLD;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_attribute
|
||||||
|
WHERE attrelid = fk.child_table
|
||||||
|
AND attname = 'deleted_at'
|
||||||
|
AND NOT attisdropped
|
||||||
|
) INTO child_has_deleted_at;
|
||||||
|
|
||||||
|
IF fk.confdeltype IN ('r', 'a') THEN
|
||||||
|
sql := format(
|
||||||
|
'SELECT EXISTS (SELECT 1 FROM %s WHERE %I = $1 %s)',
|
||||||
|
fk.child_table,
|
||||||
|
child_column,
|
||||||
|
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
||||||
|
);
|
||||||
|
EXECUTE sql INTO ref_exists USING parent_value;
|
||||||
|
IF ref_exists THEN
|
||||||
|
RAISE EXCEPTION 'Cannot soft delete %, still referenced by %',
|
||||||
|
TG_TABLE_NAME, fk.child_table;
|
||||||
|
END IF;
|
||||||
|
ELSIF fk.confdeltype = 'n' THEN
|
||||||
|
sql := format(
|
||||||
|
'UPDATE %s SET %I = NULL WHERE %I = $1 %s',
|
||||||
|
fk.child_table,
|
||||||
|
child_column,
|
||||||
|
child_column,
|
||||||
|
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
||||||
|
);
|
||||||
|
EXECUTE sql USING parent_value;
|
||||||
|
ELSIF fk.confdeltype = 'c' THEN
|
||||||
|
IF child_has_deleted_at THEN
|
||||||
|
sql := format(
|
||||||
|
'UPDATE %s SET deleted_at = NOW() WHERE %I = $1 AND deleted_at IS NULL',
|
||||||
|
fk.child_table,
|
||||||
|
child_column
|
||||||
|
);
|
||||||
|
EXECUTE sql USING parent_value;
|
||||||
|
ELSE
|
||||||
|
sql := format(
|
||||||
|
'DELETE FROM %s WHERE %I = $1',
|
||||||
|
fk.child_table,
|
||||||
|
child_column
|
||||||
|
);
|
||||||
|
EXECUTE sql USING parent_value;
|
||||||
|
END IF;
|
||||||
|
ELSIF fk.confdeltype = 'd' THEN
|
||||||
|
sql := format(
|
||||||
|
'UPDATE %s SET %I = DEFAULT WHERE %I = $1 %s',
|
||||||
|
fk.child_table,
|
||||||
|
child_column,
|
||||||
|
child_column,
|
||||||
|
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
||||||
|
);
|
||||||
|
EXECUTE sql USING parent_value;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
r record;
|
||||||
|
trigger_name text;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN
|
||||||
|
SELECT table_schema, table_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE column_name = 'deleted_at'
|
||||||
|
AND table_schema = 'public'
|
||||||
|
GROUP BY table_schema, table_name
|
||||||
|
LOOP
|
||||||
|
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
|
||||||
|
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
|
||||||
|
EXECUTE format(
|
||||||
|
'CREATE TRIGGER %I BEFORE UPDATE OF deleted_at ON %I.%I FOR EACH ROW EXECUTE FUNCTION soft_delete_handle_fk()',
|
||||||
|
trigger_name,
|
||||||
|
r.table_schema,
|
||||||
|
r.table_name
|
||||||
|
);
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
CREATE OR REPLACE FUNCTION soft_delete_handle_fk() RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
fk record;
|
||||||
|
child_column text;
|
||||||
|
parent_column text;
|
||||||
|
parent_value text;
|
||||||
|
child_has_deleted_at boolean;
|
||||||
|
ref_exists boolean;
|
||||||
|
sql text;
|
||||||
|
child_type text;
|
||||||
|
BEGIN
|
||||||
|
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
|
||||||
|
FOR fk IN
|
||||||
|
SELECT conrelid::regclass AS child_table,
|
||||||
|
conkey AS child_cols,
|
||||||
|
confkey AS parent_cols,
|
||||||
|
confdeltype
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE contype = 'f'
|
||||||
|
AND confrelid = TG_RELID
|
||||||
|
LOOP
|
||||||
|
IF array_length(fk.child_cols, 1) IS DISTINCT FROM 1
|
||||||
|
OR array_length(fk.parent_cols, 1) IS DISTINCT FROM 1 THEN
|
||||||
|
RAISE NOTICE 'soft_delete_handle_fk skipped composite fk on %', fk.child_table;
|
||||||
|
CONTINUE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT attname INTO child_column
|
||||||
|
FROM pg_attribute
|
||||||
|
WHERE attrelid = fk.child_table
|
||||||
|
AND attnum = fk.child_cols[1]
|
||||||
|
AND NOT attisdropped;
|
||||||
|
|
||||||
|
SELECT attname INTO parent_column
|
||||||
|
FROM pg_attribute
|
||||||
|
WHERE attrelid = TG_RELID
|
||||||
|
AND attnum = fk.parent_cols[1]
|
||||||
|
AND NOT attisdropped;
|
||||||
|
|
||||||
|
SELECT format_type(atttypid, atttypmod) INTO child_type
|
||||||
|
FROM pg_attribute
|
||||||
|
WHERE attrelid = fk.child_table
|
||||||
|
AND attname = child_column
|
||||||
|
AND NOT attisdropped;
|
||||||
|
|
||||||
|
IF child_type IS NULL THEN
|
||||||
|
child_type := 'text';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
EXECUTE format('SELECT ($1).%I', parent_column)
|
||||||
|
INTO parent_value
|
||||||
|
USING OLD;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_attribute
|
||||||
|
WHERE attrelid = fk.child_table
|
||||||
|
AND attname = 'deleted_at'
|
||||||
|
AND NOT attisdropped
|
||||||
|
) INTO child_has_deleted_at;
|
||||||
|
|
||||||
|
IF fk.confdeltype IN ('r', 'a') THEN
|
||||||
|
sql := format(
|
||||||
|
'SELECT EXISTS (SELECT 1 FROM %s WHERE %I = $1::%s %s)',
|
||||||
|
fk.child_table,
|
||||||
|
child_column,
|
||||||
|
child_type,
|
||||||
|
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
||||||
|
);
|
||||||
|
EXECUTE sql INTO ref_exists USING parent_value;
|
||||||
|
IF ref_exists THEN
|
||||||
|
RAISE EXCEPTION 'Cannot soft delete %, still referenced by %',
|
||||||
|
TG_TABLE_NAME, fk.child_table;
|
||||||
|
END IF;
|
||||||
|
ELSIF fk.confdeltype = 'n' THEN
|
||||||
|
sql := format(
|
||||||
|
'UPDATE %s SET %I = NULL WHERE %I = $1::%s %s',
|
||||||
|
fk.child_table,
|
||||||
|
child_column,
|
||||||
|
child_column,
|
||||||
|
child_type,
|
||||||
|
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
||||||
|
);
|
||||||
|
EXECUTE sql USING parent_value;
|
||||||
|
ELSIF fk.confdeltype = 'c' THEN
|
||||||
|
IF child_has_deleted_at THEN
|
||||||
|
sql := format(
|
||||||
|
'UPDATE %s SET deleted_at = NOW() WHERE %I = $1::%s AND deleted_at IS NULL',
|
||||||
|
fk.child_table,
|
||||||
|
child_column,
|
||||||
|
child_type
|
||||||
|
);
|
||||||
|
EXECUTE sql USING parent_value;
|
||||||
|
ELSE
|
||||||
|
sql := format(
|
||||||
|
'DELETE FROM %s WHERE %I = $1::%s',
|
||||||
|
fk.child_table,
|
||||||
|
child_column,
|
||||||
|
child_type
|
||||||
|
);
|
||||||
|
EXECUTE sql USING parent_value;
|
||||||
|
END IF;
|
||||||
|
ELSIF fk.confdeltype = 'd' THEN
|
||||||
|
sql := format(
|
||||||
|
'UPDATE %s SET %I = DEFAULT WHERE %I = $1::%s %s',
|
||||||
|
fk.child_table,
|
||||||
|
child_column,
|
||||||
|
child_column,
|
||||||
|
child_type,
|
||||||
|
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
|
||||||
|
);
|
||||||
|
EXECUTE sql USING parent_value;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
r record;
|
||||||
|
trigger_name text;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN
|
||||||
|
SELECT table_schema, table_name
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE column_name = 'deleted_at'
|
||||||
|
AND table_schema = 'public'
|
||||||
|
GROUP BY table_schema, table_name
|
||||||
|
LOOP
|
||||||
|
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
|
||||||
|
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
|
||||||
|
EXECUTE format(
|
||||||
|
'CREATE TRIGGER %I BEFORE UPDATE OF deleted_at ON %I.%I FOR EACH ROW EXECUTE FUNCTION soft_delete_handle_fk()',
|
||||||
|
trigger_name,
|
||||||
|
r.table_schema,
|
||||||
|
r.table_name
|
||||||
|
);
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_project_flock_kandang_uniformity_project_flock_kandang'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
DROP CONSTRAINT fk_project_flock_kandang_uniformity_project_flock_kandang;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
ADD CONSTRAINT fk_project_flock_kandang_uniformity_project_flock_kandang
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs (id)
|
||||||
|
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE tablename = 'project_budgets'
|
||||||
|
) THEN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_project_budgets_project_flock_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE project_budgets
|
||||||
|
DROP CONSTRAINT fk_project_budgets_project_flock_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ALTER TABLE project_budgets
|
||||||
|
ADD CONSTRAINT fk_project_budgets_project_flock_id
|
||||||
|
FOREIGN KEY (project_flock_id)
|
||||||
|
REFERENCES project_flocks(id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE tablename = 'project_flock_kandang_uniformity'
|
||||||
|
) THEN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'project_flock_kandang_uniformity'
|
||||||
|
AND column_name = 'created_at'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
ADD COLUMN created_at TIMESTAMPTZ DEFAULT NOW();
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'project_flock_kandang_uniformity'
|
||||||
|
AND column_name = 'updated_at'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
ADD COLUMN updated_at TIMESTAMPTZ DEFAULT NOW();
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'project_flock_kandang_uniformity'
|
||||||
|
AND column_name = 'deleted_at'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
ADD COLUMN deleted_at TIMESTAMPTZ;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_project_flock_kandang_uniformity_project_flock_kandang'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
DROP CONSTRAINT fk_project_flock_kandang_uniformity_project_flock_kandang;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
ADD CONSTRAINT fk_project_flock_kandang_uniformity_project_flock_kandang
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs (id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_tables
|
||||||
|
WHERE tablename = 'project_budgets'
|
||||||
|
) THEN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_project_budgets_project_flock_id'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE project_budgets
|
||||||
|
DROP CONSTRAINT fk_project_budgets_project_flock_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ALTER TABLE project_budgets
|
||||||
|
ADD CONSTRAINT fk_project_budgets_project_flock_id
|
||||||
|
FOREIGN KEY (project_flock_id)
|
||||||
|
REFERENCES project_flocks(id)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_trigger
|
||||||
|
WHERE tgname = 'trg_soft_delete_fk_project_flock_kandang_uniformity'
|
||||||
|
) THEN
|
||||||
|
DROP TRIGGER trg_soft_delete_fk_project_flock_kandang_uniformity
|
||||||
|
ON project_flock_kandang_uniformity;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'project_flock_kandang_uniformity'
|
||||||
|
AND column_name = 'created_at'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
DROP COLUMN created_at;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'project_flock_kandang_uniformity'
|
||||||
|
AND column_name = 'updated_at'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
DROP COLUMN updated_at;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'project_flock_kandang_uniformity'
|
||||||
|
AND column_name = 'deleted_at'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE project_flock_kandang_uniformity
|
||||||
|
DROP COLUMN deleted_at;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
import (
|
import "time"
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProjectFlockKandangUniformity struct {
|
type ProjectFlockKandangUniformity struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
@@ -18,9 +14,6 @@ type ProjectFlockKandangUniformity struct {
|
|||||||
UniformQty float64 `gorm:"type:numeric(15,3)"`
|
UniformQty float64 `gorm:"type:numeric(15,3)"`
|
||||||
NotUniformQty float64 `gorm:"type:numeric(15,3)"`
|
NotUniformQty float64 `gorm:"type:numeric(15,3)"`
|
||||||
UniformDate *time.Time `gorm:"type:timestamptz"`
|
UniformDate *time.Time `gorm:"type:timestamptz"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
|
|
||||||
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
|
|||||||
@@ -194,13 +194,17 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
StockLogId: newLog.Id,
|
StockLogId: newLog.Id,
|
||||||
ProductWarehouseId: productWarehouse.Id,
|
ProductWarehouseId: productWarehouse.Id,
|
||||||
}
|
}
|
||||||
|
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
|
||||||
|
s.Log.Errorf("Failed to create adjustment stock: %+v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
|
||||||
|
}
|
||||||
|
|
||||||
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
||||||
// Adjustment INCREASE → Replenish stock (Stockable)
|
// Adjustment INCREASE → Replenish stock (Stockable)
|
||||||
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
|
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
|
||||||
replenishResult, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
|
_, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
|
||||||
StockableKey: "ADJUSTMENT_IN",
|
StockableKey: "ADJUSTMENT_IN",
|
||||||
StockableID: newLog.Id,
|
StockableID: adjustmentStock.Id,
|
||||||
ProductWarehouseID: uint(productWarehouse.Id),
|
ProductWarehouseID: uint(productWarehouse.Id),
|
||||||
Quantity: req.Quantity,
|
Quantity: req.Quantity,
|
||||||
Note: ¬e,
|
Note: ¬e,
|
||||||
@@ -210,15 +214,11 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock via FIFO: %v", err))
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock via FIFO: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update stockable tracking fields
|
|
||||||
adjustmentStock.TotalQty = replenishResult.AddedQuantity
|
|
||||||
adjustmentStock.TotalUsed = 0
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Adjustment DECREASE → Consume stock (Usable)
|
// Adjustment DECREASE → Consume stock (Usable)
|
||||||
consumeResult, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
|
_, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
|
||||||
UsableKey: "ADJUSTMENT_OUT",
|
UsableKey: "ADJUSTMENT_OUT",
|
||||||
UsableID: newLog.Id,
|
UsableID: adjustmentStock.Id,
|
||||||
ProductWarehouseID: uint(productWarehouse.Id),
|
ProductWarehouseID: uint(productWarehouse.Id),
|
||||||
Quantity: req.Quantity,
|
Quantity: req.Quantity,
|
||||||
AllowPending: false, // Don't allow pending for adjustment
|
AllowPending: false, // Don't allow pending for adjustment
|
||||||
@@ -227,16 +227,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to consume stock via FIFO: %v", err))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to consume stock via FIFO: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update usable tracking fields
|
|
||||||
adjustmentStock.UsageQty = consumeResult.UsageQuantity
|
|
||||||
adjustmentStock.PendingQty = consumeResult.PendingQuantity
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save AdjustmentStock record
|
|
||||||
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
|
|
||||||
s.Log.Errorf("Failed to create adjustment stock: %+v", err)
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update ProductWarehouse quantity (for backward compatibility/reporting)
|
// Update ProductWarehouse quantity (for backward compatibility/reporting)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
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"
|
||||||
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"
|
||||||
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
|
uniformityRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
|
||||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
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"
|
||||||
|
|
||||||
@@ -866,6 +867,14 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(pfkIDs) > 0 {
|
if len(pfkIDs) > 0 {
|
||||||
|
uniformityRepo := uniformityRepository.NewUniformityRepository(s.Repository.DB())
|
||||||
|
if dbTransaction != nil {
|
||||||
|
uniformityRepo = uniformityRepository.NewUniformityRepository(dbTransaction)
|
||||||
|
}
|
||||||
|
if err := uniformityRepo.DeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove uniformity data for project flock kandang")
|
||||||
|
}
|
||||||
|
|
||||||
pwRepo := s.ProductWarehouseRepo
|
pwRepo := s.ProductWarehouseRepo
|
||||||
if dbTransaction != nil {
|
if dbTransaction != nil {
|
||||||
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
|
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
|
||||||
|
|||||||
@@ -295,16 +295,17 @@ func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm.
|
|||||||
|
|
||||||
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||||
var rows []struct {
|
var rows []struct {
|
||||||
UsageQty float64
|
TotalQty float64
|
||||||
UomName string
|
UomName string
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.
|
if err := tx.
|
||||||
Table("recording_stocks").
|
Table("recording_stocks").
|
||||||
Select("COALESCE(recording_stocks.usage_qty, 0) AS usage_qty, LOWER(uoms.name) AS uom_name").
|
Select("COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0) AS total_qty, LOWER(uoms.name) AS uom_name").
|
||||||
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
|
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
|
||||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
Joins("JOIN uoms ON uoms.id = products.uom_id").
|
Joins("JOIN uoms ON uoms.id = products.uom_id").
|
||||||
|
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ? AND UPPER(flags.name) = ?", entity.FlagableTypeProduct, "PAKAN").
|
||||||
Where("recording_stocks.recording_id = ?", recordingID).
|
Where("recording_stocks.recording_id = ?", recordingID).
|
||||||
Scan(&rows).Error; err != nil {
|
Scan(&rows).Error; err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -312,16 +313,16 @@ func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID u
|
|||||||
|
|
||||||
var total float64
|
var total float64
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
if row.UsageQty <= 0 {
|
if row.TotalQty <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch strings.TrimSpace(row.UomName) {
|
switch strings.TrimSpace(row.UomName) {
|
||||||
case "kilogram", "kg", "kilograms", "kilo":
|
case "kilogram", "kg", "kilograms", "kilo":
|
||||||
total += row.UsageQty * 1000
|
total += row.TotalQty * 1000
|
||||||
case "gram", "g", "grams":
|
case "gram", "g", "grams":
|
||||||
total += row.UsageQty
|
total += row.TotalQty
|
||||||
default:
|
default:
|
||||||
total += row.UsageQty
|
total += row.TotalQty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return total, nil
|
return total, nil
|
||||||
|
|||||||
@@ -229,7 +229,13 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.CreateOne(ctx, &createdRecording, func(*gorm.DB) *gorm.DB { return tx }); err != nil {
|
createTx := tx.WithContext(ctx).Select(
|
||||||
|
"ProjectFlockKandangId",
|
||||||
|
"RecordDatetime",
|
||||||
|
"Day",
|
||||||
|
"CreatedBy",
|
||||||
|
)
|
||||||
|
if err := createTx.Create(&createdRecording).Error; err != nil {
|
||||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
return fiber.NewError(
|
return fiber.NewError(
|
||||||
fiber.StatusBadRequest,
|
fiber.StatusBadRequest,
|
||||||
@@ -241,11 +247,13 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
}
|
}
|
||||||
|
|
||||||
mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks)
|
mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks)
|
||||||
|
stockDesired := resetStockQuantitiesForFIFO(mappedStocks, s.FifoSvc != nil)
|
||||||
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
|
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
|
||||||
s.Log.Errorf("Failed to persist stocks: %+v", err)
|
s.Log.Errorf("Failed to persist stocks: %+v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyStockDesiredQuantities(mappedStocks, stockDesired, s.FifoSvc != nil)
|
||||||
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
|
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -299,9 +307,11 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
|
|
||||||
var recordingEntity *entity.Recording
|
var recordingEntity *entity.Recording
|
||||||
|
var updatedRecording *entity.Recording
|
||||||
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
recording, err := s.Repository.GetByID(ctx, id, func(db *gorm.DB) *gorm.DB {
|
repoTx := s.Repository.WithTx(tx)
|
||||||
return s.Repository.WithRelations(tx)
|
recording, err := repoTx.GetByID(ctx, id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.Repository.WithRelations(db)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -316,6 +326,42 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
hasDepletionChanges := req.Depletions != nil
|
hasDepletionChanges := req.Depletions != nil
|
||||||
hasEggChanges := req.Eggs != nil
|
hasEggChanges := req.Eggs != nil
|
||||||
|
|
||||||
|
var existingStocks []entity.RecordingStock
|
||||||
|
if hasStockChanges {
|
||||||
|
existingStocks, err = s.Repository.ListStocks(tx, recordingEntity.Id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to list existing stocks: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if stocksMatch(existingStocks, req.Stocks) {
|
||||||
|
hasStockChanges = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingDepletions []entity.RecordingDepletion
|
||||||
|
if hasDepletionChanges {
|
||||||
|
existingDepletions, err = s.Repository.ListDepletions(tx, recordingEntity.Id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to list existing depletions: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if depletionsMatch(existingDepletions, req.Depletions) {
|
||||||
|
hasDepletionChanges = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingEggs []entity.RecordingEgg
|
||||||
|
if hasEggChanges {
|
||||||
|
existingEggs, err = s.Repository.ListEggs(tx, recordingEntity.Id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to list existing eggs: %+v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if eggsMatch(existingEggs, req.Eggs) {
|
||||||
|
hasEggChanges = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !hasStockChanges && !hasDepletionChanges && !hasEggChanges {
|
if !hasStockChanges && !hasDepletionChanges && !hasEggChanges {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -347,39 +393,12 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hasStockChanges {
|
if hasStockChanges {
|
||||||
existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id)
|
if err := s.syncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks); err != nil {
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to list existing stocks: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.releaseRecordingStocks(ctx, tx, existingStocks); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.DeleteStocks(tx, recordingEntity.Id); err != nil {
|
|
||||||
s.Log.Errorf("Failed to clear stocks: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
mappedStocks := recordingutil.MapStocks(recordingEntity.Id, req.Stocks)
|
|
||||||
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
|
|
||||||
s.Log.Errorf("Failed to update stocks: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasDepletionChanges {
|
if hasDepletionChanges {
|
||||||
existingDepletions, err := s.Repository.ListDepletions(tx, recordingEntity.Id)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to list existing depletions: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil {
|
if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil {
|
||||||
s.Log.Errorf("Failed to clear depletions: %+v", err)
|
s.Log.Errorf("Failed to clear depletions: %+v", err)
|
||||||
return err
|
return err
|
||||||
@@ -398,12 +417,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hasEggChanges {
|
if hasEggChanges {
|
||||||
existingEggs, err := s.Repository.ListEggs(tx, recordingEntity.Id)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to list existing eggs: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.DeleteEggs(tx, recordingEntity.Id); err != nil {
|
if err := s.Repository.DeleteEggs(tx, recordingEntity.Id); err != nil {
|
||||||
s.Log.Errorf("Failed to clear eggs: %+v", err)
|
s.Log.Errorf("Failed to clear eggs: %+v", err)
|
||||||
return err
|
return err
|
||||||
@@ -421,7 +434,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasStockChanges || hasDepletionChanges {
|
if hasStockChanges || hasDepletionChanges || hasEggChanges {
|
||||||
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
|
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
|
||||||
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
||||||
return err
|
return err
|
||||||
@@ -470,13 +483,31 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updated, err := repoTx.GetByID(ctx, recordingEntity.Id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.Repository.WithRelations(db)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to reload recording %d after update: %+v", recordingEntity.Id, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updatedRecording = updated
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if transactionErr != nil {
|
if transactionErr != nil {
|
||||||
return nil, transactionErr
|
return nil, transactionErr
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.GetOne(c, id)
|
if updatedRecording == nil {
|
||||||
|
return s.GetOne(c, id)
|
||||||
|
}
|
||||||
|
if err := s.attachLatestApproval(ctx, updatedRecording); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := s.attachProductionStandard(ctx, updatedRecording); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return updatedRecording, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error) {
|
func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error) {
|
||||||
@@ -654,12 +685,17 @@ func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.
|
|||||||
if stock.UsageQty != nil {
|
if stock.UsageQty != nil {
|
||||||
desired = *stock.UsageQty
|
desired = *stock.UsageQty
|
||||||
}
|
}
|
||||||
|
var pending float64
|
||||||
|
if stock.PendingQty != nil {
|
||||||
|
pending = *stock.PendingQty
|
||||||
|
}
|
||||||
|
desiredTotal := desired + pending
|
||||||
|
|
||||||
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
||||||
UsableKey: recordingStockUsableKey,
|
UsableKey: recordingStockUsableKey,
|
||||||
UsableID: stock.Id,
|
UsableID: stock.Id,
|
||||||
ProductWarehouseID: stock.ProductWarehouseId,
|
ProductWarehouseID: stock.ProductWarehouseId,
|
||||||
Quantity: desired,
|
Quantity: desiredTotal,
|
||||||
AllowPending: true,
|
AllowPending: true,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
})
|
})
|
||||||
@@ -745,6 +781,288 @@ func (s *recordingService) adjustProductWarehouseQuantities(ctx context.Context,
|
|||||||
return s.ProductWarehouseRepo.AdjustQuantities(ctx, deltas, func(*gorm.DB) *gorm.DB { return tx })
|
return s.ProductWarehouseRepo.AdjustQuantities(ctx, deltas, func(*gorm.DB) *gorm.DB { return tx })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type desiredStock struct {
|
||||||
|
Usage float64
|
||||||
|
Pending float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetStockQuantitiesForFIFO(stocks []entity.RecordingStock, enabled bool) []desiredStock {
|
||||||
|
desired := make([]desiredStock, len(stocks))
|
||||||
|
for i := range stocks {
|
||||||
|
if stocks[i].UsageQty != nil {
|
||||||
|
desired[i].Usage = *stocks[i].UsageQty
|
||||||
|
}
|
||||||
|
if stocks[i].PendingQty != nil {
|
||||||
|
desired[i].Pending = *stocks[i].PendingQty
|
||||||
|
}
|
||||||
|
if !enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
zero := 0.0
|
||||||
|
stocks[i].UsageQty = &zero
|
||||||
|
stocks[i].PendingQty = &zero
|
||||||
|
}
|
||||||
|
return desired
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyStockDesiredQuantities(stocks []entity.RecordingStock, desired []desiredStock, enabled bool) {
|
||||||
|
if !enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range stocks {
|
||||||
|
if i >= len(desired) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
usage := desired[i].Usage
|
||||||
|
pending := desired[i].Pending
|
||||||
|
stocks[i].UsageQty = &usage
|
||||||
|
stocks[i].PendingQty = &pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) syncRecordingStocks(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
recordingID uint,
|
||||||
|
existing []entity.RecordingStock,
|
||||||
|
incoming []validation.Stock,
|
||||||
|
) error {
|
||||||
|
if s.FifoSvc == nil {
|
||||||
|
if err := s.Repository.DeleteStocks(tx, recordingID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mapped := recordingutil.MapStocks(recordingID, incoming)
|
||||||
|
return s.Repository.CreateStocks(tx, mapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
existingByWarehouse := make(map[uint][]entity.RecordingStock)
|
||||||
|
for _, stock := range existing {
|
||||||
|
existingByWarehouse[stock.ProductWarehouseId] = append(existingByWarehouse[stock.ProductWarehouseId], stock)
|
||||||
|
}
|
||||||
|
|
||||||
|
stocksToConsume := make([]entity.RecordingStock, 0, len(incoming))
|
||||||
|
for _, item := range incoming {
|
||||||
|
list := existingByWarehouse[item.ProductWarehouseId]
|
||||||
|
var stock entity.RecordingStock
|
||||||
|
if len(list) > 0 {
|
||||||
|
stock = list[0]
|
||||||
|
existingByWarehouse[item.ProductWarehouseId] = list[1:]
|
||||||
|
} else {
|
||||||
|
zero := 0.0
|
||||||
|
stock = entity.RecordingStock{
|
||||||
|
RecordingId: recordingID,
|
||||||
|
ProductWarehouseId: item.ProductWarehouseId,
|
||||||
|
UsageQty: &zero,
|
||||||
|
PendingQty: &zero,
|
||||||
|
}
|
||||||
|
if err := tx.Create(&stock).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
desired := item.Qty
|
||||||
|
stock.UsageQty = &desired
|
||||||
|
if item.PendingQty != nil {
|
||||||
|
pending := *item.PendingQty
|
||||||
|
stock.PendingQty = &pending
|
||||||
|
}
|
||||||
|
stocksToConsume = append(stocksToConsume, stock)
|
||||||
|
}
|
||||||
|
|
||||||
|
var leftovers []entity.RecordingStock
|
||||||
|
for _, list := range existingByWarehouse {
|
||||||
|
leftovers = append(leftovers, list...)
|
||||||
|
}
|
||||||
|
if len(leftovers) > 0 {
|
||||||
|
if err := s.releaseRecordingStocks(ctx, tx, leftovers); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ids := make([]uint, 0, len(leftovers))
|
||||||
|
for _, stock := range leftovers {
|
||||||
|
if stock.Id != 0 {
|
||||||
|
ids = append(ids, stock.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(ids) > 0 {
|
||||||
|
if err := tx.Where("id IN ?", ids).Delete(&entity.RecordingStock{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stocksToConsume) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.consumeRecordingStocks(ctx, tx, stocksToConsume)
|
||||||
|
}
|
||||||
|
|
||||||
|
type eggTotals struct {
|
||||||
|
Qty int
|
||||||
|
Weight float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type stockTotals struct {
|
||||||
|
Usage float64
|
||||||
|
Pending float64
|
||||||
|
Total float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func summarizeExistingStocks(stocks []entity.RecordingStock) map[uint]stockTotals {
|
||||||
|
totals := make(map[uint]stockTotals)
|
||||||
|
for _, stock := range stocks {
|
||||||
|
var usage float64
|
||||||
|
var pending float64
|
||||||
|
if stock.UsageQty != nil {
|
||||||
|
usage = *stock.UsageQty
|
||||||
|
}
|
||||||
|
if stock.PendingQty != nil {
|
||||||
|
pending = *stock.PendingQty
|
||||||
|
}
|
||||||
|
current := totals[stock.ProductWarehouseId]
|
||||||
|
current.Usage += usage
|
||||||
|
current.Pending += pending
|
||||||
|
current.Total += usage + pending
|
||||||
|
totals[stock.ProductWarehouseId] = current
|
||||||
|
}
|
||||||
|
return totals
|
||||||
|
}
|
||||||
|
|
||||||
|
func summarizeIncomingStocks(stocks []validation.Stock) map[uint]stockTotals {
|
||||||
|
totals := make(map[uint]stockTotals)
|
||||||
|
for _, stock := range stocks {
|
||||||
|
var pending float64
|
||||||
|
if stock.PendingQty != nil {
|
||||||
|
pending = *stock.PendingQty
|
||||||
|
}
|
||||||
|
current := totals[stock.ProductWarehouseId]
|
||||||
|
current.Usage += stock.Qty
|
||||||
|
current.Pending += pending
|
||||||
|
current.Total += stock.Qty + pending
|
||||||
|
totals[stock.ProductWarehouseId] = current
|
||||||
|
}
|
||||||
|
return totals
|
||||||
|
}
|
||||||
|
|
||||||
|
func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool {
|
||||||
|
hasPending := false
|
||||||
|
for _, item := range incoming {
|
||||||
|
if item.PendingQty != nil {
|
||||||
|
hasPending = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
existingUsage := make(map[uint]float64)
|
||||||
|
existingTotal := make(map[uint]float64)
|
||||||
|
for _, stock := range existing {
|
||||||
|
var usage float64
|
||||||
|
var pending float64
|
||||||
|
if stock.UsageQty != nil {
|
||||||
|
usage = *stock.UsageQty
|
||||||
|
}
|
||||||
|
if stock.PendingQty != nil {
|
||||||
|
pending = *stock.PendingQty
|
||||||
|
}
|
||||||
|
existingUsage[stock.ProductWarehouseId] += usage
|
||||||
|
existingTotal[stock.ProductWarehouseId] += usage + pending
|
||||||
|
}
|
||||||
|
|
||||||
|
incomingUsage := make(map[uint]float64)
|
||||||
|
incomingTotal := make(map[uint]float64)
|
||||||
|
for _, item := range incoming {
|
||||||
|
var pending float64
|
||||||
|
if item.PendingQty != nil {
|
||||||
|
pending = *item.PendingQty
|
||||||
|
}
|
||||||
|
incomingUsage[item.ProductWarehouseId] += item.Qty
|
||||||
|
incomingTotal[item.ProductWarehouseId] += item.Qty + pending
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasPending {
|
||||||
|
return floatMapsMatch(existingTotal, incomingTotal)
|
||||||
|
}
|
||||||
|
return floatMapsMatch(existingUsage, incomingUsage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func depletionsMatch(existing []entity.RecordingDepletion, incoming []validation.Depletion) bool {
|
||||||
|
existingTotals := make(map[uint]float64)
|
||||||
|
for _, dep := range existing {
|
||||||
|
existingTotals[dep.ProductWarehouseId] += dep.Qty
|
||||||
|
}
|
||||||
|
|
||||||
|
incomingTotals := make(map[uint]float64)
|
||||||
|
for _, dep := range incoming {
|
||||||
|
incomingTotals[dep.ProductWarehouseId] += dep.Qty
|
||||||
|
}
|
||||||
|
|
||||||
|
return floatMapsMatch(existingTotals, incomingTotals)
|
||||||
|
}
|
||||||
|
|
||||||
|
func eggsMatch(existing []entity.RecordingEgg, incoming []validation.Egg) bool {
|
||||||
|
existingTotals := make(map[uint]eggTotals)
|
||||||
|
for _, egg := range existing {
|
||||||
|
weight := 0.0
|
||||||
|
if egg.Weight != nil {
|
||||||
|
weight = *egg.Weight
|
||||||
|
}
|
||||||
|
current := existingTotals[egg.ProductWarehouseId]
|
||||||
|
current.Qty += egg.Qty
|
||||||
|
current.Weight += float64(egg.Qty) * weight
|
||||||
|
existingTotals[egg.ProductWarehouseId] = current
|
||||||
|
}
|
||||||
|
|
||||||
|
incomingTotals := make(map[uint]eggTotals)
|
||||||
|
for _, egg := range incoming {
|
||||||
|
weight := 0.0
|
||||||
|
if egg.Weight != nil {
|
||||||
|
weight = *egg.Weight
|
||||||
|
}
|
||||||
|
current := incomingTotals[egg.ProductWarehouseId]
|
||||||
|
current.Qty += egg.Qty
|
||||||
|
current.Weight += float64(egg.Qty) * weight
|
||||||
|
incomingTotals[egg.ProductWarehouseId] = current
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(existingTotals) != len(incomingTotals) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, existingTotal := range existingTotals {
|
||||||
|
incomingTotal, ok := incomingTotals[key]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if existingTotal.Qty != incomingTotal.Qty {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !floatNearlyEqual(existingTotal.Weight, incomingTotal.Weight) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func floatMapsMatch(a, b map[uint]float64) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for key, value := range a {
|
||||||
|
other, ok := b[key]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !floatNearlyEqual(value, other) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func floatNearlyEqual(a, b float64) bool {
|
||||||
|
return math.Abs(a-b) <= 0.000001
|
||||||
|
}
|
||||||
|
|
||||||
func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm.DB, recording *entity.Recording) error {
|
func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm.DB, recording *entity.Recording) error {
|
||||||
day := 0
|
day := 0
|
||||||
if recording.Day != nil {
|
if recording.Day != nil {
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ func (u *UniformityController) GetOne(c *fiber.Ctx) error {
|
|||||||
Uniformity: result.Uniformity,
|
Uniformity: result.Uniformity,
|
||||||
Cv: result.Cv,
|
Cv: result.Cv,
|
||||||
}
|
}
|
||||||
|
document, documentURL, err = u.UniformityService.GetDocumentInfo(c, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
standard, err := u.UniformityService.GetStandard(c, result)
|
standard, err := u.UniformityService.GetStandard(c, result)
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ type UniformityListDTO struct {
|
|||||||
MeanDown float64 `json:"mean_down"`
|
MeanDown float64 `json:"mean_down"`
|
||||||
StandardMeanWeight *float64 `json:"standard_mean_weight"`
|
StandardMeanWeight *float64 `json:"standard_mean_weight"`
|
||||||
StandardUniformity *float64 `json:"standard_uniformity"`
|
StandardUniformity *float64 `json:"standard_uniformity"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
CreatedBy uint `json:"created_by"`
|
CreatedBy uint `json:"created_by"`
|
||||||
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
||||||
}
|
}
|
||||||
@@ -154,7 +153,6 @@ func ToUniformityListDTOs(items []entity.ProjectFlockKandangUniformity) []Unifor
|
|||||||
UniformQty: item.UniformQty,
|
UniformQty: item.UniformQty,
|
||||||
MeanUp: item.MeanUp,
|
MeanUp: item.MeanUp,
|
||||||
MeanDown: item.MeanDown,
|
MeanDown: item.MeanDown,
|
||||||
CreatedAt: item.CreatedAt,
|
|
||||||
CreatedBy: item.CreatedBy,
|
CreatedBy: item.CreatedBy,
|
||||||
LatestApproval: latestApproval,
|
LatestApproval: latestApproval,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 @@ import (
|
|||||||
|
|
||||||
type UniformityRepository interface {
|
type UniformityRepository interface {
|
||||||
repository.BaseRepository[entity.ProjectFlockKandangUniformity]
|
repository.BaseRepository[entity.ProjectFlockKandangUniformity]
|
||||||
|
DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type UniformityRepositoryImpl struct {
|
type UniformityRepositoryImpl struct {
|
||||||
@@ -19,3 +22,13 @@ func NewUniformityRepository(db *gorm.DB) UniformityRepository {
|
|||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockKandangUniformity](db),
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockKandangUniformity](db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *UniformityRepositoryImpl) DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error {
|
||||||
|
if len(projectFlockKandangIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.DB().WithContext(ctx).
|
||||||
|
Unscoped().
|
||||||
|
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
|
||||||
|
Delete(&entity.ProjectFlockKandangUniformity{}).Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type UniformityService interface {
|
|||||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error)
|
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error)
|
||||||
ParseBodyWeightExcel(ctx *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error)
|
ParseBodyWeightExcel(ctx *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error)
|
||||||
ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error)
|
ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error)
|
||||||
|
GetDocumentInfo(ctx *fiber.Ctx, uniformityID uint) (*entity.Document, string, error)
|
||||||
CalculateUniformityFromDocument(ctx *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, string, error)
|
CalculateUniformityFromDocument(ctx *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]ent
|
|||||||
if params.Week != 0 {
|
if params.Week != 0 {
|
||||||
db = db.Where("week = ?", params.Week)
|
db = db.Where("week = ?", params.Week)
|
||||||
}
|
}
|
||||||
return db.Order("uniform_date DESC").Order("created_at DESC")
|
return db.Order("uniform_date DESC").Order("id DESC")
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -592,28 +593,19 @@ func (s uniformityService) ComputeUniformity(rows []BodyWeightExcelRow) (Uniform
|
|||||||
return computeUniformity(rows)
|
return computeUniformity(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s uniformityService) GetDocumentInfo(c *fiber.Ctx, uniformityID uint) (*entity.Document, string, error) {
|
||||||
|
return s.fetchUniformityDocument(c.Context(), uniformityID, true)
|
||||||
|
}
|
||||||
|
|
||||||
func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, string, error) {
|
func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, string, error) {
|
||||||
if s.DocumentSvc == nil {
|
document, url, err := s.fetchUniformityDocument(c.Context(), uniformityID, false)
|
||||||
return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
documents, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(uniformityID))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return UniformityCalculation{}, nil, "", err
|
return UniformityCalculation{}, nil, "", err
|
||||||
}
|
}
|
||||||
if len(documents) == 0 {
|
if document == nil || url == "" {
|
||||||
return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
|
return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
document := documents[0]
|
|
||||||
url, err := s.DocumentSvc.PresignURL(c.Context(), document, 15*time.Minute)
|
|
||||||
if err != nil {
|
|
||||||
return UniformityCalculation{}, nil, "", err
|
|
||||||
}
|
|
||||||
if url == "" {
|
|
||||||
return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusBadRequest, "Uniformity document URL not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(c.Context(), http.MethodGet, url, nil)
|
req, err := http.NewRequestWithContext(c.Context(), http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return UniformityCalculation{}, nil, "", err
|
return UniformityCalculation{}, nil, "", err
|
||||||
@@ -638,7 +630,35 @@ func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniform
|
|||||||
return UniformityCalculation{}, nil, "", err
|
return UniformityCalculation{}, nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return calculation, &document, url, nil
|
return calculation, document, url, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s uniformityService) fetchUniformityDocument(ctx context.Context, uniformityID uint, allowMissing bool) (*entity.Document, string, error) {
|
||||||
|
if s.DocumentSvc == nil {
|
||||||
|
return nil, "", fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
documents, err := s.DocumentSvc.ListByTarget(ctx, "UNIFORMITY", uint64(uniformityID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if len(documents) == 0 {
|
||||||
|
if allowMissing {
|
||||||
|
return nil, "", nil
|
||||||
|
}
|
||||||
|
return nil, "", fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
document := documents[0]
|
||||||
|
url, err := s.DocumentSvc.PresignURL(ctx, document, 15*time.Minute)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if url == "" {
|
||||||
|
return nil, "", fiber.NewError(fiber.StatusBadRequest, "Uniformity document URL not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &document, url, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *uniformityService) createUniformityApproval(
|
func (s *uniformityService) createUniformityApproval(
|
||||||
|
|||||||
@@ -180,7 +180,10 @@ func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
|
|||||||
req.Items = []validation.ReceivePurchaseItemRequest{singleItem}
|
req.Items = []validation.ReceivePurchaseItemRequest{singleItem}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
req.TravelDocuments = form.File["documents"]
|
req.TravelDocuments = form.File["travel_documents"]
|
||||||
|
if len(req.TravelDocuments) == 0 {
|
||||||
|
req.TravelDocuments = form.File["documents"]
|
||||||
|
}
|
||||||
result, err := ctrl.service.ReceiveProducts(c, uint(id), req)
|
result, err := ctrl.service.ReceiveProducts(c, uint(id), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -999,6 +999,22 @@ func (s *purchaseService) uploadTravelDocument(
|
|||||||
return "", errors.New("document service not available")
|
return "", errors.New("document service not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
documents, err := s.DocumentSvc.ListByTarget(ctx, string(utils.DocumentableTypePurchaseItem), uint64(itemID))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(documents) > 0 {
|
||||||
|
var ids []uint
|
||||||
|
for _, doc := range documents {
|
||||||
|
if doc.Type == string(utils.DocumentTypePurchaseTravel) {
|
||||||
|
ids = append(ids, doc.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.DocumentSvc.DeleteDocuments(ctx, ids, true); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
documentFiles := []commonSvc.DocumentFile{{
|
documentFiles := []commonSvc.DocumentFile{{
|
||||||
File: file,
|
File: file,
|
||||||
Type: string(utils.DocumentTypePurchaseTravel),
|
Type: string(utils.DocumentTypePurchaseTravel),
|
||||||
@@ -1015,7 +1031,7 @@ func (s *purchaseService) uploadTravelDocument(
|
|||||||
if len(results) == 0 {
|
if len(results) == 0 {
|
||||||
return "", errors.New("upload result is empty")
|
return "", errors.New("upload result is empty")
|
||||||
}
|
}
|
||||||
return results[0].URL, nil
|
return results[0].Document.Path, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error) {
|
func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error) {
|
||||||
@@ -1499,10 +1515,56 @@ func (s *purchaseService) loadPurchase(
|
|||||||
if err := s.attachLatestApproval(ctx, purchase); err != nil {
|
if err := s.attachLatestApproval(ctx, purchase); err != nil {
|
||||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err)
|
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err)
|
||||||
}
|
}
|
||||||
|
s.applyTravelDocumentURLs(ctx, purchase)
|
||||||
|
|
||||||
return purchase, nil
|
return purchase, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *purchaseService) applyTravelDocumentURLs(ctx context.Context, purchase *entity.Purchase) {
|
||||||
|
if purchase == nil || s.DocumentSvc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range purchase.Items {
|
||||||
|
item := &purchase.Items[i]
|
||||||
|
documents, err := s.DocumentSvc.ListByTarget(ctx, string(utils.DocumentableTypePurchaseItem), uint64(item.Id))
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Warnf("Unable to load travel documents for purchase item %d: %+v", item.Id, err)
|
||||||
|
} else {
|
||||||
|
var targetDoc *entity.Document
|
||||||
|
for j := len(documents) - 1; j >= 0; j-- {
|
||||||
|
if documents[j].Type == string(utils.DocumentTypePurchaseTravel) {
|
||||||
|
targetDoc = &documents[j]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if targetDoc != nil {
|
||||||
|
url, err := s.DocumentSvc.PresignURL(ctx, *targetDoc, 15*time.Minute)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Warnf("Unable to presign travel document for purchase item %d: %+v", item.Id, err)
|
||||||
|
} else if url != "" {
|
||||||
|
item.TravelNumberDocs = &url
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path := item.TravelNumberDocs
|
||||||
|
if path == nil || strings.TrimSpace(*path) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
url, err := commonSvc.ResolveDocumentURL(ctx, s.DocumentSvc, *path, 15*time.Minute)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Warnf("Unable to presign travel document for purchase item %d: %+v", item.Id, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if url == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item.TravelNumberDocs = &url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func collectPFKIDsFromPurchase(p *entity.Purchase) []uint {
|
func collectPFKIDsFromPurchase(p *entity.Purchase) []uint {
|
||||||
seen := make(map[uint]struct{})
|
seen := make(map[uint]struct{})
|
||||||
ids := make([]uint, 0)
|
ids := make([]uint, 0)
|
||||||
|
|||||||
@@ -171,6 +171,9 @@ func (h *Controller) Refresh(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
utils.Log.Warnf("token refresh response status %d", resp.StatusCode)
|
utils.Log.Warnf("token refresh response status %d", resp.StatusCode)
|
||||||
|
if resp.StatusCode == fiber.StatusTooManyRequests {
|
||||||
|
return fiber.NewError(fiber.StatusTooManyRequests, "Too many attempts, please slow down")
|
||||||
|
}
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated")
|
return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (r *UserRepositoryImpl) GetByIdUser(
|
|||||||
modifier func(*gorm.DB) *gorm.DB,
|
modifier func(*gorm.DB) *gorm.DB,
|
||||||
) (*entity.User, error) {
|
) (*entity.User, error) {
|
||||||
return r.BaseRepositoryImpl.First(ctx, func(db *gorm.DB) *gorm.DB {
|
return r.BaseRepositoryImpl.First(ctx, func(db *gorm.DB) *gorm.DB {
|
||||||
return db.Where("id_user = ?", idUser)
|
return db.Where("id_user::bigint = ?::bigint", idUser)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ func (r *UserRepositoryImpl) UpsertByIdUser(ctx context.Context, user *entity.Us
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepositoryImpl) SoftDeleteByIdUser(ctx context.Context, idUser int64) error {
|
func (r *UserRepositoryImpl) SoftDeleteByIdUser(ctx context.Context, idUser int64) error {
|
||||||
query := r.DB().WithContext(ctx).Where("id_user = ?", idUser)
|
query := r.DB().WithContext(ctx).Where("id_user::bigint = ?::bigint", idUser)
|
||||||
result := query.Delete(&entity.User{})
|
result := query.Delete(&entity.User{})
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return result.Error
|
return result.Error
|
||||||
|
|||||||
@@ -426,12 +426,12 @@ const (
|
|||||||
DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT"
|
DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT"
|
||||||
DocumentTypeExpense DocumentType = "EXPENSE_DOCUMENT"
|
DocumentTypeExpense DocumentType = "EXPENSE_DOCUMENT"
|
||||||
DocumentTypeExpenseRealization DocumentType = "EXPENSE_REALIZATION_DOCUMENT"
|
DocumentTypeExpenseRealization DocumentType = "EXPENSE_REALIZATION_DOCUMENT"
|
||||||
DocumentTypePurchaseTravel DocumentType = "PURCHASE_TRAVEL_DOCUMENT"
|
DocumentTypePurchaseTravel DocumentType = "PURCHASE_TRAVEL_DOCUMENT"
|
||||||
|
|
||||||
DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER"
|
DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER"
|
||||||
DocumentableTypeExpense DocumentableType = "EXPENSE"
|
DocumentableTypeExpense DocumentableType = "EXPENSE"
|
||||||
DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION"
|
DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION"
|
||||||
DocumentableTypePurchaseItem DocumentableType = "PURCHASE_ITEM"
|
DocumentableTypePurchaseItem DocumentableType = "PURCHASE_ITEM"
|
||||||
)
|
)
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user