mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-23 23:05:44 +00:00
feat(BE): integrate FIFO service for stock adjustments and transfers
- Added FIFO service integration in the adjustments module to manage stockable and usable items for adjustments. - Created a new repository for adjustment stocks to handle database operations. - Enhanced the adjustment service to track stock adjustments using FIFO logic for both increase and decrease operations. - Updated product warehouse DTOs and repositories to include project flock information. - Implemented FIFO logic in the transfer module to manage stock transfers between warehouses. - Added integration tests for FIFO operations in stock transfers, ensuring correct stock consumption and replenishment.
This commit is contained in:
@@ -44,9 +44,10 @@ type transferService struct {
|
||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||
DocumentSvc commonSvc.DocumentService
|
||||
FifoSvc commonSvc.FifoService
|
||||
}
|
||||
|
||||
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService) TransferService {
|
||||
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, fifoSvc commonSvc.FifoService) TransferService {
|
||||
return &transferService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
@@ -60,6 +61,7 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
DocumentSvc: documentSvc,
|
||||
FifoSvc: fifoSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +128,7 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
|
||||
|
||||
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error) {
|
||||
|
||||
// === VALIDASI SOURCE WAREHOUSE ===
|
||||
pwIDs := make([]uint, 0, len(req.Products))
|
||||
|
||||
for _, product := range req.Products {
|
||||
@@ -152,6 +155,21 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
return nil, err
|
||||
}
|
||||
|
||||
destPfkID, err := s.getActiveProjectFlockKandangID(c.Context(), uint(req.DestinationWarehouseID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.ProjectFlockKandangRepo != nil {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -206,14 +224,62 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
return err
|
||||
}
|
||||
|
||||
var details []*entity.StockTransferDetail
|
||||
// Prepare details and fetch product warehouses
|
||||
details := make([]*entity.StockTransferDetail, 0, len(req.Products))
|
||||
detailMap := make(map[uint64]*entity.StockTransferDetail)
|
||||
|
||||
for _, product := range req.Products {
|
||||
details = append(details, &entity.StockTransferDetail{
|
||||
// Get source product warehouse
|
||||
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
||||
c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID),
|
||||
)
|
||||
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.StatusInternalServerError, "Gagal mengambil data product warehouse source")
|
||||
}
|
||||
|
||||
// Get or create destination product warehouse
|
||||
destPW, err := s.ProductWarehouseRepo.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")
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
ctx := c.Context()
|
||||
projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.DestinationWarehouseID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destPW = &entity.ProductWarehouse{
|
||||
ProductId: uint(product.ProductID),
|
||||
WarehouseId: uint(req.DestinationWarehouseID),
|
||||
Quantity: 0,
|
||||
ProjectFlockKandangId: &projectFlockKandangID,
|
||||
}
|
||||
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat product warehouse destination")
|
||||
}
|
||||
}
|
||||
|
||||
detail := &entity.StockTransferDetail{
|
||||
StockTransferId: entityTransfer.Id,
|
||||
ProductId: uint64(product.ProductID),
|
||||
Quantity: product.ProductQty,
|
||||
})
|
||||
|
||||
SourceProductWarehouseID: func() *uint64 { id := uint64(sourcePW.Id); return &id }(),
|
||||
UsageQty: 0,
|
||||
PendingQty: 0,
|
||||
|
||||
DestProductWarehouseID: func() *uint64 { id := uint64(destPW.Id); return &id }(),
|
||||
TotalQty: 0,
|
||||
TotalUsed: 0,
|
||||
}
|
||||
details = append(details, detail)
|
||||
detailMap[uint64(product.ProductID)] = detail
|
||||
}
|
||||
|
||||
if err := s.StockTransferDetailRepo.WithTx(tx).CreateMany(c.Context(), details, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -233,23 +299,18 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
return err
|
||||
}
|
||||
|
||||
detailMap := make(map[uint64]uint64)
|
||||
for _, d := range details {
|
||||
detailMap[d.ProductId] = d.Id
|
||||
}
|
||||
|
||||
var deliveryItems []*entity.StockTransferDeliveryItem
|
||||
|
||||
for i, delivery := range deliveries {
|
||||
item := req.Deliveries[i]
|
||||
for _, prod := range item.Products {
|
||||
detailID, ok := detailMap[uint64(prod.ProductID)]
|
||||
detail, ok := detailMap[uint64(prod.ProductID)]
|
||||
if !ok {
|
||||
return fmt.Errorf("produk %d tidak ditemukan di detail", prod.ProductID)
|
||||
}
|
||||
deliveryItems = append(deliveryItems, &entity.StockTransferDeliveryItem{
|
||||
StockTransferDeliveryId: delivery.Id,
|
||||
StockTransferDetailId: detailID,
|
||||
StockTransferDetailId: detail.Id,
|
||||
Quantity: prod.ProductQty,
|
||||
})
|
||||
}
|
||||
@@ -280,69 +341,54 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
}
|
||||
|
||||
// Execute FIFO operations for each product
|
||||
for _, product := range req.Products {
|
||||
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID))
|
||||
detail := detailMap[uint64(product.ProductID)]
|
||||
|
||||
// Step 1: Consume stock from source warehouse (STOCK_TRANSFER_OUT)
|
||||
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
|
||||
UsableKey: "STOCK_TRANSFER_OUT",
|
||||
UsableID: uint(detail.Id),
|
||||
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
|
||||
Quantity: product.ProductQty,
|
||||
AllowPending: false, // Don't allow pending, must have actual stock
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source product warehouse")
|
||||
}
|
||||
if sourcePW.Quantity < product.ProductQty {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock in source warehouse for product ID: %d", product.ProductID))
|
||||
}
|
||||
sourcePW.Quantity -= product.ProductQty
|
||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), sourcePW.Id, sourcePW, nil); err != nil {
|
||||
return err
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak cukup di gudang asal untuk produk %d: %v", product.ProductID, err))
|
||||
}
|
||||
|
||||
decreaseLog := &entity.StockLog{
|
||||
Decrease: product.ProductQty,
|
||||
Notes: "",
|
||||
LoggableType: string(utils.StockLogTypeTransfer),
|
||||
LoggableId: uint(entityTransfer.Id),
|
||||
ProductWarehouseId: sourcePW.Id,
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), decreaseLog, nil); err != nil {
|
||||
return err
|
||||
// Update usage tracking fields for source warehouse
|
||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||
Where("id = ?", detail.Id).
|
||||
Updates(map[string]interface{}{
|
||||
"usage_qty": consumeResult.UsageQuantity,
|
||||
"pending_qty": consumeResult.PendingQuantity,
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("gagal update usage tracking: %w", err)
|
||||
}
|
||||
|
||||
destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
||||
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
||||
)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get destination product warehouse")
|
||||
}
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
ctx := c.Context()
|
||||
projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.DestinationWarehouseID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destPW = &entity.ProductWarehouse{
|
||||
ProductId: uint(product.ProductID),
|
||||
WarehouseId: uint(req.DestinationWarehouseID),
|
||||
Quantity: 0,
|
||||
ProjectFlockKandangId: &projectFlockKandangID,
|
||||
}
|
||||
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create destination product warehouse")
|
||||
}
|
||||
// Step 2: Replenish stock to destination warehouse (STOCK_TRANSFER_IN)
|
||||
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
||||
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||
StockableKey: "STOCK_TRANSFER_IN",
|
||||
StockableID: uint(detail.Id),
|
||||
ProductWarehouseID: uint(*detail.DestProductWarehouseID),
|
||||
Quantity: product.ProductQty,
|
||||
Note: ¬e,
|
||||
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))
|
||||
}
|
||||
|
||||
destPW.Quantity += product.ProductQty
|
||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), destPW.Id, destPW, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
increaseLog := &entity.StockLog{
|
||||
Increase: product.ProductQty,
|
||||
LoggableType: string(utils.StockLogTypeTransfer),
|
||||
LoggableId: uint(entityTransfer.Id),
|
||||
Notes: "",
|
||||
ProductWarehouseId: destPW.Id,
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), increaseLog, nil); err != nil {
|
||||
return err
|
||||
// Update total tracking fields for destination warehouse
|
||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||
Where("id = ?", detail.Id).
|
||||
Updates(map[string]interface{}{
|
||||
"total_qty": replenishResult.AddedQuantity,
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("gagal update total tracking: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user