From 7183df6938421d144784197dfbf9f931f8c396d2 Mon Sep 17 00:00:00 2001 From: giovanni Date: Wed, 4 Feb 2026 09:17:16 +0700 Subject: [PATCH] add query adjustment stock at closing sapronak --- ...260203034048_add_field_adj_number.down.sql | 6 + ...20260203034048_add_field_adj_number.up.sql | 10 ++ internal/entities/adjustment_stock.go | 1 + .../repositories/closing.repository.go | 133 ++++++++++++++++-- .../adjustment_stock.repository.go | 73 ++++++++++ .../services/adjustment.service.go | 5 + internal/utils/constant.go | 7 +- 7 files changed, 219 insertions(+), 16 deletions(-) create mode 100644 internal/database/migrations/20260203034048_add_field_adj_number.down.sql create mode 100644 internal/database/migrations/20260203034048_add_field_adj_number.up.sql diff --git a/internal/database/migrations/20260203034048_add_field_adj_number.down.sql b/internal/database/migrations/20260203034048_add_field_adj_number.down.sql new file mode 100644 index 00000000..48bb2b54 --- /dev/null +++ b/internal/database/migrations/20260203034048_add_field_adj_number.down.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE adjustment_stocks +DROP COLUMN adj_number; + +COMMIT; diff --git a/internal/database/migrations/20260203034048_add_field_adj_number.up.sql b/internal/database/migrations/20260203034048_add_field_adj_number.up.sql new file mode 100644 index 00000000..1517bbea --- /dev/null +++ b/internal/database/migrations/20260203034048_add_field_adj_number.up.sql @@ -0,0 +1,10 @@ +BEGIN; + +ALTER TABLE adjustment_stocks +ADD COLUMN adj_number VARCHAR(255); + +UPDATE adjustment_stocks +SET adj_number = CONCAT('ADJ-', LPAD(id::text, 5, '0')) +WHERE adj_number IS NULL; + +COMMIT; diff --git a/internal/entities/adjustment_stock.go b/internal/entities/adjustment_stock.go index 841e4820..9ccf9246 100644 --- a/internal/entities/adjustment_stock.go +++ b/internal/entities/adjustment_stock.go @@ -11,6 +11,7 @@ type AdjustmentStock struct { PendingQty float64 `gorm:"column:pending_qty;default:0"` CreatedAt time.Time `gorm:"column:created_at;autoCreateTime"` UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime"` + AdjNumber string `gorm:"column:adj_number;uniqueIndex;not null"` ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` StockLog *StockLog `gorm:"polymorphic:Loggable;polymorphicType:LoggableType;polymorphicId:LoggableId;polymorphicValue:ADJUSTMENT"` diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index cd5ce2da..9b7c5bff 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -102,12 +102,12 @@ func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params Sapronak if len(params.WarehouseIDs) == 0 { return []SapronakRow{}, 0, nil } - unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL) - args = append(args, params.WarehouseIDs, params.WarehouseIDs) + unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL) + args = append(args, params.WarehouseIDs, params.WarehouseIDs, params.WarehouseIDs) case validation.SapronakTypeOutgoing: if len(params.WarehouseIDs) > 0 { - unionParts = append(unionParts, sapronakOutgoingTransfersSQL) - args = append(args, params.WarehouseIDs) + unionParts = append(unionParts, sapronakOutgoingTransfersSQL, sapronakOutgoingAdjustmentsSQL) + args = append(args, params.WarehouseIDs, params.WarehouseIDs) } if len(params.ProjectFlockKandangIDs) > 0 { unionParts = append(unionParts, sapronakOutgoingMarketingsSQL) @@ -174,12 +174,12 @@ func (r *ClosingRepositoryImpl) GetSapronakSummary(ctx context.Context, params S if len(params.WarehouseIDs) == 0 { return []SapronakSummaryRow{}, nil } - unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL) - args = append(args, params.WarehouseIDs, params.WarehouseIDs) + unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL, sapronakIncomingAdjustmentsSQL) + args = append(args, params.WarehouseIDs, params.WarehouseIDs, params.WarehouseIDs) case validation.SapronakTypeOutgoing: if len(params.WarehouseIDs) > 0 { - unionParts = append(unionParts, sapronakOutgoingTransfersSQL) - args = append(args, params.WarehouseIDs) + unionParts = append(unionParts, sapronakOutgoingTransfersSQL, sapronakOutgoingAdjustmentsSQL) + args = append(args, params.WarehouseIDs, params.WarehouseIDs) } if len(params.ProjectFlockKandangIDs) > 0 { unionParts = append(unionParts, sapronakOutgoingMarketingsSQL) @@ -456,7 +456,7 @@ SELECT COALESCE(pi.received_date, '1970-01-01') AS sort_date, COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text, COALESCE(p.po_number, '') AS reference_number, - 'Purchase' AS transaction_type, + 'Pembelian' AS transaction_type, prod.name AS product_name, COALESCE(( SELECT string_agg( @@ -505,7 +505,7 @@ SELECT st.transfer_date AS sort_date, TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text, st.movement_number AS reference_number, - 'Internal Transfer In' AS transaction_type, + 'Mutasi' AS transaction_type, prod.name AS product_name, COALESCE(( SELECT string_agg( @@ -549,13 +549,63 @@ JOIN uoms u ON u.id = prod.uom_id WHERE st.to_warehouse_id IN ? ` + sapronakIncomingAdjustmentsSQL = ` +SELECT + CAST(ast.id AS BIGINT) AS id, + ast.created_at AS sort_date, + COALESCE(TO_CHAR(ast.created_at, 'DD-Mon-YYYY'), '') AS date_text, + COALESCE(ast.adj_number, '') AS reference_number, + 'Adjustment stock' AS transaction_type, + prod.name AS product_name, + COALESCE(( + SELECT string_agg( + f.name, + ' ' ORDER BY + CASE + WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 + ELSE 1 + END, + f.name + ) + FROM flags f + WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id + ), '') AS product_category, + COALESCE(( + SELECT string_agg( + f.name, + ' ' ORDER BY + CASE + WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 + ELSE 1 + END, + f.name + ) + FROM flags f + WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id + ), '') AS product_sub_category, + COALESCE(w.name, '') AS source_warehouse, + '-' AS destination_warehouse, + '' AS destination, + COALESCE(ast.total_qty, 0) AS quantity, + u.id AS unit_id, + u.name AS unit, + '-' AS notes +FROM adjustment_stocks ast +JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id +JOIN warehouses w ON w.id = pw.warehouse_id +JOIN products prod ON prod.id = pw.product_id +JOIN uoms u ON u.id = prod.uom_id +WHERE pw.warehouse_id IN ? + AND COALESCE(ast.total_qty, 0) <> 0 +` + sapronakOutgoingTransfersSQL = ` SELECT CAST(st.id AS BIGINT) AS id, st.transfer_date AS sort_date, TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text, st.movement_number AS reference_number, - 'Internal Transfer Out' AS transaction_type, + 'Mutasi' AS transaction_type, prod.name AS product_name, COALESCE(( SELECT string_agg( @@ -599,13 +649,70 @@ JOIN uoms u ON u.id = prod.uom_id WHERE st.from_warehouse_id IN ? ` + sapronakOutgoingAdjustmentsSQL = ` +SELECT + CAST(ast.id AS BIGINT) AS id, + ast.created_at AS sort_date, + COALESCE(TO_CHAR(ast.created_at, 'DD-Mon-YYYY'), '') AS date_text, + COALESCE(ast.adj_number, '') AS reference_number, + 'Adjustment stock' AS transaction_type, + prod.name AS product_name, + COALESCE(( + SELECT string_agg( + f.name, + ' ' ORDER BY + CASE + WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 + ELSE 1 + END, + f.name + ) + FROM flags f + WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id + ), '') AS product_category, + COALESCE(( + SELECT string_agg( + f.name, + ' ' ORDER BY + CASE + WHEN UPPER(f.name) IN ('DOC', 'PAKAN', 'OVK', 'PULLET') THEN 0 + ELSE 1 + END, + f.name + ) + FROM flags f + WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id + ), '') AS product_sub_category, + COALESCE(w.name, '') AS source_warehouse, + '-' AS destination_warehouse, + '' AS destination, + COALESCE(ast.usage_qty, 0) AS quantity, + u.id AS unit_id, + u.name AS unit, + '-' AS notes +FROM adjustment_stocks ast +JOIN product_warehouses pw ON pw.id = ast.product_warehouse_id +JOIN warehouses w ON w.id = pw.warehouse_id +JOIN products prod ON prod.id = pw.product_id +JOIN uoms u ON u.id = prod.uom_id +WHERE pw.warehouse_id IN ? + AND COALESCE(ast.usage_qty, 0) <> 0 + AND EXISTS ( + SELECT 1 + FROM flags f + WHERE f.flagable_id = pw.product_id + AND f.flagable_type = 'products' + AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK') + ) +` + sapronakOutgoingMarketingsSQL = ` SELECT CAST(mp.id AS BIGINT) AS id, m.so_date AS sort_date, TO_CHAR(m.so_date, 'DD-Mon-YYYY') AS date_text, m.so_number AS reference_number, - 'Trading Sales' AS transaction_type, + 'Penjualan' AS transaction_type, prod.name AS product_name, COALESCE(( SELECT string_agg( @@ -653,7 +760,7 @@ WHERE pw.project_flock_kandang_id IN ? FROM flags f WHERE f.flagable_id = pw.product_id AND f.flagable_type = 'products' - AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET') + AND UPPER(f.name) NOT IN ('DOC', 'LAYER', 'PULLET', 'AYAM-AFKIR', 'AYAM-MATI', 'AYAM-CULLING', 'TELUR-UTUH', 'TELUR-PECAH', 'TELUR-PUTIH', 'TELUR-RETAK') ) ` ) diff --git a/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go b/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go index f62738a3..9409fd73 100644 --- a/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go +++ b/internal/modules/inventory/adjustments/repositories/adjustment_stock.repository.go @@ -2,9 +2,13 @@ package repositories import ( "context" + "fmt" + "strconv" + "strings" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" + "gorm.io/gorm/clause" ) type AdjustmentStockRepository interface { @@ -12,6 +16,7 @@ type AdjustmentStockRepository interface { GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.AdjustmentStock, error) WithTx(tx *gorm.DB) AdjustmentStockRepository DB() *gorm.DB + GenerateSequentialNumber(ctx context.Context, prefix string) (string, error) } type adjustmentStockRepositoryImpl struct { @@ -50,3 +55,71 @@ func (r *adjustmentStockRepositoryImpl) WithTx(tx *gorm.DB) AdjustmentStockRepos func (r *adjustmentStockRepositoryImpl) DB() *gorm.DB { return r.db } + +func (r *adjustmentStockRepositoryImpl) GenerateSequentialNumber(ctx context.Context, prefix string) (string, error) { + var values []string + err := r.db.WithContext(ctx). + Model(&entity.AdjustmentStock{}). + Where(fmt.Sprintf("%s ILIKE ?", "adj_number"), prefix+"%"). + Select("adj_number"). + Order(fmt.Sprintf("%s DESC", "adj_number")). + Limit(20). + Clauses(clause.Locking{Strength: "UPDATE"}). + Pluck("adj_number", &values).Error + if err != nil { + return "", err + } + + next := 1 + for _, value := range values { + if number, ok := parseNumericSuffix(value, prefix); ok { + next = number + 1 + break + } + } + + const maxAttempts = 20 + for attempt := 0; attempt < maxAttempts; attempt++ { + candidate := fmt.Sprintf("%s%0*d", prefix, 5, next) + exists, err := r.numberExists(ctx, r.db, candidate) + if err != nil { + return "", err + } + if !exists { + return candidate, nil + } + next++ + } + + return "", fmt.Errorf("unable to generate unique %s", "adj_number") +} + +func (r *adjustmentStockRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, value string) (bool, error) { + var count int64 + if err := db.WithContext(ctx). + Model(&entity.AdjustmentStock{}). + Where(fmt.Sprintf("%s = ?", "adj_number"), value). + Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +func parseNumericSuffix(value, prefix string) (int, bool) { + if !strings.HasPrefix(value, prefix) { + return 0, false + } + suffix := strings.TrimPrefix(value, prefix) + if suffix == "" { + return 0, false + } + trimmed := strings.TrimLeft(suffix, "0") + if trimmed == "" { + trimmed = "0" + } + number, err := strconv.Atoi(trimmed) + if err != nil { + return 0, false + } + return number, true +} diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index 862d6991..a763a6c6 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -200,6 +200,11 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e adjustmentStock := &entity.AdjustmentStock{ ProductWarehouseId: productWarehouse.Id, } + code, err := s.AdjustmentStockRepository.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix) + if err != nil { + return err + } + adjustmentStock.AdjNumber = code if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record") diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 9abd6a30..3b8ee054 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -329,9 +329,10 @@ const ( PurchaseStepReceiving approvalutils.ApprovalStep = 4 PurchaseStepCompleted approvalutils.ApprovalStep = 5 - PurchasePRNumberPrefix = "PR-LTI-" - PurchasePONumberPrefix = "PO-LTI-" - PurchaseNumberPadding = 4 + PurchasePRNumberPrefix = "PR-LTI-" + PurchasePONumberPrefix = "PO-LTI-" + AdjustmentStockNumberPrefix = "ADJ-" + PurchaseNumberPadding = 4 ) var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{