|
|
|
@@ -113,7 +113,11 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
|
|
|
|
Where("w_from.location_id IN ? OR w_to.location_id IN ?", scope.IDs, scope.IDs)
|
|
|
|
|
}
|
|
|
|
|
if params.Search != "" {
|
|
|
|
|
db = db.Where("movement_number ILIKE ?", "%"+strings.TrimSpace(params.Search)+"%")
|
|
|
|
|
searchTerm := "%" + strings.TrimSpace(params.Search) + "%"
|
|
|
|
|
db = db.Joins("LEFT JOIN warehouses AS from_warehouses ON from_warehouses.id = stock_transfers.from_warehouse_id").
|
|
|
|
|
Joins("LEFT JOIN warehouses AS to_warehouses ON to_warehouses.id = stock_transfers.to_warehouse_id").
|
|
|
|
|
Where("movement_number ILIKE ? OR from_warehouses.name ILIKE ? OR to_warehouses.name ILIKE ?",
|
|
|
|
|
searchTerm, searchTerm, searchTerm)
|
|
|
|
|
}
|
|
|
|
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
|
|
|
|
})
|
|
|
|
@@ -154,9 +158,10 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Transfer dengan ID %d tidak ditemukan", id))
|
|
|
|
|
}
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
|
|
|
|
|
s.Log.Errorf("Failed to fetch transfer by ID %d: %+v", id, err)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data transfer")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return transferPtr, nil
|
|
|
|
@@ -172,12 +177,13 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk dengan ID %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID))
|
|
|
|
|
}
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek stok produk di gudang asal")
|
|
|
|
|
s.Log.Errorf("Failed to fetch product warehouse for product_id=%d, warehouse_id=%d: %+v", product.ProductID, req.SourceWarehouseID, err)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengecek stok produk")
|
|
|
|
|
}
|
|
|
|
|
if sourcePW.Quantity < product.ProductQty {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak mencukupi. Tersedia: %.2f, Diminta: %.2f", product.ProductID, sourcePW.Quantity, product.ProductQty))
|
|
|
|
|
}
|
|
|
|
|
pwIDs = append(pwIDs, sourcePW.Id)
|
|
|
|
|
}
|
|
|
|
@@ -195,12 +201,15 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
|
|
|
|
|
}
|
|
|
|
|
if projectFlockKandang.ClosedAt != nil {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock tujuan sudah closing")
|
|
|
|
|
if destPfkID > 0 {
|
|
|
|
|
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), destPfkID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.Log.Errorf("Failed to fetch project flock kandang by ID %d: %+v", destPfkID, err)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
|
|
|
|
|
}
|
|
|
|
|
if projectFlockKandang.ClosedAt != nil {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Project flock untuk gudang tujuan sudah ditutup (closing) pada %s", projectFlockKandang.ClosedAt.Format("2006-01-02")))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
actorID, err := m.ActorIDFromContext(c)
|
|
|
|
@@ -228,16 +237,18 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d tidak ditemukan", delivery.SupplierID))
|
|
|
|
|
}
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal cek data supplier")
|
|
|
|
|
s.Log.Errorf("Failed to fetch supplier by ID %d: %+v", delivery.SupplierID, err)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data supplier")
|
|
|
|
|
}
|
|
|
|
|
if supplier.Category != string(utils.SupplierCategoryBOP) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier dengan ID %d bukan kategori BOP", delivery.SupplierID))
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Supplier '%s' (ID: %d) bukan kategori BOP. Kategori saat ini: %s", supplier.Name, delivery.SupplierID, supplier.Category))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
movementNumber, err := s.StockTransferRepo.GenerateMovementNumber(c.Context())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
|
|
|
|
|
s.Log.Errorf("Failed to generate movement number: %+v", err)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat nomor transfer")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
transferDate, _ := utils.ParseDateString(req.TransferDate)
|
|
|
|
@@ -275,16 +286,18 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d tidak tersedia di gudang asal", product.ProductID))
|
|
|
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan di gudang asal (ID: %d)", product.ProductID, req.SourceWarehouseID))
|
|
|
|
|
}
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse source")
|
|
|
|
|
s.Log.Errorf("Failed to fetch source product warehouse for product_id=%d, warehouse_id=%d: %+v", product.ProductID, req.SourceWarehouseID, err)
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data stok gudang asal")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
destPW, err := productWarehouseRepoTX.GetProductWarehouseByProductAndWarehouseID(
|
|
|
|
|
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
|
|
|
|
)
|
|
|
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data product warehouse destination")
|
|
|
|
|
s.Log.Errorf("Failed to fetch dest product warehouse for product_id=%d, warehouse_id=%d: %+v", product.ProductID, req.DestinationWarehouseID, err)
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data stok gudang tujuan")
|
|
|
|
|
}
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
ctx := c.Context()
|
|
|
|
@@ -292,14 +305,21 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var pfkID *uint
|
|
|
|
|
if projectFlockKandangID > 0 {
|
|
|
|
|
pfkID = &projectFlockKandangID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
destPW = &entity.ProductWarehouse{
|
|
|
|
|
ProductId: uint(product.ProductID),
|
|
|
|
|
WarehouseId: uint(req.DestinationWarehouseID),
|
|
|
|
|
Quantity: 0,
|
|
|
|
|
ProjectFlockKandangId: &projectFlockKandangID,
|
|
|
|
|
ProjectFlockKandangId: pfkID,
|
|
|
|
|
}
|
|
|
|
|
if err := productWarehouseRepoTX.CreateOne(c.Context(), destPW, nil); err != nil {
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat product warehouse destination")
|
|
|
|
|
s.Log.Errorf("Failed to create product warehouse for product_id=%d, warehouse_id=%d: %+v", product.ProductID, req.DestinationWarehouseID, err)
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat data stok gudang tujuan")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -345,7 +365,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|
|
|
|
for _, prod := range item.Products {
|
|
|
|
|
detail, ok := detailMap[uint64(prod.ProductID)]
|
|
|
|
|
if !ok {
|
|
|
|
|
return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID)
|
|
|
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Produk %d tidak ditemukan dalam daftar transfer untuk delivery #%d", prod.ProductID, i+1))
|
|
|
|
|
}
|
|
|
|
|
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
|
|
|
|
|
StockTransferDeliveryId: delivery.Id,
|
|
|
|
@@ -389,9 +409,9 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|
|
|
|
Files: documentFiles,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.Log.WithError(err).Errorf("Failed to upload document for delivery %d (delivery_id: %d, filename: %s)",
|
|
|
|
|
deliveryIdx+1, delivery.Id, file.Filename)
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d: %v", deliveryIdx+1, err))
|
|
|
|
|
s.Log.Errorf("Failed to upload document for delivery %d (delivery_id=%d, filename=%s): %+v",
|
|
|
|
|
deliveryIdx+1, delivery.Id, file.Filename, err)
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengunggah dokumen")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -408,7 +428,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|
|
|
|
Tx: tx,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak cukup di gudang asal untuk produk %d: %v", product.ProductID, err))
|
|
|
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := tx.Model(&entity.StockTransferDetail{}).
|
|
|
|
@@ -417,7 +437,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|
|
|
|
"usage_qty": consumeResult.UsageQuantity,
|
|
|
|
|
"pending_qty": consumeResult.PendingQuantity,
|
|
|
|
|
}).Error; err != nil {
|
|
|
|
|
return fmt.Errorf("gagal update usage tracking: %w", err)
|
|
|
|
|
s.Log.Errorf("Failed to update tracking usage for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
|
|
|
@@ -430,7 +451,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|
|
|
|
Tx: tx,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok di gudang tujuan untuk produk %d: %v", product.ProductID, err))
|
|
|
|
|
s.Log.Errorf("Failed to replenish stock for product_id=%d, pw_id=%d, qty=%.2f: %+v", product.ProductID, *detail.DestProductWarehouseID, product.ProductQty, err)
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal menambah stok gudang tujuan")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := tx.Model(&entity.StockTransferDetail{}).
|
|
|
|
@@ -438,7 +460,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|
|
|
|
Updates(map[string]interface{}{
|
|
|
|
|
"total_qty": replenishResult.AddedQuantity,
|
|
|
|
|
}).Error; err != nil {
|
|
|
|
|
return fmt.Errorf("gagal update total tracking: %w", err)
|
|
|
|
|
s.Log.Errorf("Failed to update tracking total for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -472,7 +495,10 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err))
|
|
|
|
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
|
|
|
|
return nil, fiberErr
|
|
|
|
|
}
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result, err := s.GetOne(c, uint(entityTransfer.Id))
|
|
|
|
@@ -482,8 +508,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|
|
|
|
|
|
|
|
|
if len(expensePayloads) > 0 {
|
|
|
|
|
if err := s.notifyExpenseItemsDelivered(c, entityTransfer.Id, expensePayloads); err != nil {
|
|
|
|
|
s.Log.Errorf("Failed to sync expense for transfer %d: %+v", entityTransfer.Id, err)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to sync expense: %v", err))
|
|
|
|
|
s.Log.Errorf("Failed to sync expense for transfer_id=%d, movement_number=%s: %+v", entityTransfer.Id, entityTransfer.MovementNumber, err)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal sinkronisasi data expense. Silakan cek manual di module expense")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -497,32 +523,27 @@ func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID u
|
|
|
|
|
return s.ExpenseBridge.OnItemsDelivered(c, transferID, payloads)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *transferService) notifyExpenseDetailsDeleted(ctx context.Context, transferID uint64, items []entity.StockTransferDetail) error {
|
|
|
|
|
if s.ExpenseBridge == nil || transferID == 0 || len(items) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return s.ExpenseBridge.OnItemsDeleted(ctx, transferID, items)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) {
|
|
|
|
|
warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
|
|
|
|
}
|
|
|
|
|
s.Log.Errorf("Failed to fetch warehouse by ID %d: %+v", warehouseID, err)
|
|
|
|
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
|
|
|
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID))
|
|
|
|
|
return 0, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId))
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
|
|
|
|
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak ada project flock aktif untuk kandang %d", *warehouse.KandangId))
|
|
|
|
|
}
|
|
|
|
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
|
|
|
|
|
s.Log.Errorf("Failed to fetch active project flock kandang for kandang_id=%d: %+v", *warehouse.KandangId, err)
|
|
|
|
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data project flock")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return uint(projectFlockKandang.Id), nil
|
|
|
|
|