diff --git a/cmd/repoint-wrong-warehouse-relations/main.go b/cmd/repoint-wrong-warehouse-relations/main.go index e39c5b0b..2e1b2484 100644 --- a/cmd/repoint-wrong-warehouse-relations/main.go +++ b/cmd/repoint-wrong-warehouse-relations/main.go @@ -26,12 +26,13 @@ const ( ) type options struct { - Apply bool - Output string - AreaName string - KandangLocationName string - DBSSLMode string - DeleteWrongWarehouses bool + Apply bool + Output string + AreaName string + KandangLocationName string + DBSSLMode string + DeleteWrongWarehouses bool + AllowMovingAllocatedStocks bool } type planRow struct { @@ -123,6 +124,16 @@ func main() { log.Fatalf("failed to load plan rows: %v", err) } + if opts.AllowMovingAllocatedStocks { + allocatedRows, err := loadPlanRowsWithAllocations(ctx, db, opts) + if err != nil { + log.Fatalf("failed to load allocated plan rows: %v", err) + } + rows = append(rows, allocatedRows...) + // Remove duplicates + rows = deduplicatePlanRows(rows) + } + if len(rows) == 0 { fmt.Println("No misplaced PAKAN/OVK stocks found in wrong-location warehouses") return @@ -158,6 +169,7 @@ func parseFlags() (*options, error) { flag.StringVar(&opts.KandangLocationName, "kandang-location-name", "", "Optional exact canonical kandang location filter") flag.StringVar(&opts.DBSSLMode, "db-sslmode", "", "Optional database sslmode override, for example: require") flag.BoolVar(&opts.DeleteWrongWarehouses, "delete-wrong-warehouses", true, "Soft delete wrong warehouse rows after all references have been moved") + flag.BoolVar(&opts.AllowMovingAllocatedStocks, "allow-moving-allocated-stocks", false, "Allow moving stocks that have active allocations (use with caution - for old recordings with completed allocations)") flag.Parse() opts.Output = strings.ToLower(strings.TrimSpace(opts.Output)) @@ -175,6 +187,90 @@ func parseFlags() (*options, error) { return &opts, nil } +func deduplicatePlanRows(rows []planRow) []planRow { + seen := make(map[uint]struct{}) + result := make([]planRow, 0, len(rows)) + for _, row := range rows { + if _, ok := seen[row.SurvivorPWID]; !ok { + seen[row.SurvivorPWID] = struct{}{} + result = append(result, row) + } + } + return result +} + +func loadPlanRowsWithAllocations(ctx context.Context, db *gorm.DB, opts *options) ([]planRow, error) { + filters := make([]string, 0, 2) + args := make([]any, 0, 2) + if opts.AreaName != "" { + filters = append(filters, "a.name = ?") + args = append(args, opts.AreaName) + } + if opts.KandangLocationName != "" { + filters = append(filters, "kl.name = ?") + args = append(args, opts.KandangLocationName) + } + + query := fmt.Sprintf(` +SELECT + a.name AS area_name, + kl.name AS kandang_location_name, + k.id AS kandang_id, + k.name AS kandang_name, + w.id AS wrong_warehouse_id, + w.name AS wrong_warehouse_name, + correct_w.id AS correct_warehouse_id, + correct_w.name AS correct_warehouse_name, + p.id AS product_id, + p.name AS product_name, + wp.project_flock_kandang_id, + wp.id AS survivor_pw_id, + COALESCE(wp.qty, 0) AS survivor_current_qty, + cpw.id AS absorbed_pw_id, + cpw.qty AS absorbed_current_qty +FROM warehouses w +JOIN kandangs k + ON k.id = w.kandang_id + AND k.deleted_at IS NULL +JOIN locations kl + ON kl.id = k.location_id +JOIN areas a + ON a.id = kl.area_id +JOIN LATERAL ( + SELECT w2.id, w2.name + FROM warehouses w2 + WHERE w2.location_id = k.location_id + AND UPPER(COALESCE(w2.type, '')) = 'LOKASI' + AND w2.deleted_at IS NULL + ORDER BY w2.id ASC + LIMIT 1 +) AS correct_w ON TRUE +JOIN product_warehouses wp + ON wp.warehouse_id = w.id +JOIN products p + ON p.id = wp.product_id +JOIN flags f + ON f.flagable_id = p.id + AND f.flagable_type = 'products' + AND UPPER(f.name) IN ('PAKAN', 'OVK') +LEFT JOIN product_warehouses cpw + ON cpw.product_id = wp.product_id + AND cpw.warehouse_id = correct_w.id + AND cpw.project_flock_kandang_id IS NOT DISTINCT FROM wp.project_flock_kandang_id +WHERE w.deleted_at IS NULL + AND w.kandang_id IS NOT NULL + AND w.location_id IS DISTINCT FROM k.location_id + %s +ORDER BY a.name ASC, kl.name ASC, k.name ASC, wp.id ASC +`, andClause(filters)) + + rows := make([]planRow, 0) + if err := db.WithContext(ctx).Raw(query, args...).Scan(&rows).Error; err != nil { + return nil, err + } + return rows, nil +} + func loadPlanRows(ctx context.Context, db *gorm.DB, opts *options) ([]planRow, error) { filters := make([]string, 0, 2) args := make([]any, 0, 2)