fix: first push implementation fifo v2 to all stockable and useable

This commit is contained in:
Hafizh A. Y
2026-02-27 15:21:55 +07:00
parent a2de21e351
commit e7e065c320
28 changed files with 1469 additions and 164 deletions
@@ -179,6 +179,7 @@ func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, cu
flag == string(utils.FlagTelurPecah) ||
flag == string(utils.FlagTelurPutih) ||
flag == string(utils.FlagTelurRetak) ||
flag == string(utils.FlagAyam) ||
flag == string(utils.FlagAyamAfkir) ||
flag == string(utils.FlagAyamCulling) ||
flag == string(utils.FlagAyamMati) {
@@ -144,6 +144,7 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
for _, t := range marketingTypes {
switch t {
case string(utils.MarketingTypeAyamPullet):
flagSet[string(utils.FlagAyam)] = struct{}{}
flagSet[string(utils.FlagDOC)] = struct{}{}
flagSet[string(utils.FlagPullet)] = struct{}{}
flagSet[string(utils.FlagLayer)] = struct{}{}
@@ -50,6 +50,16 @@ type transferService struct {
ExpenseBridge TransferExpenseBridge
}
const (
transferFunctionCodeIn = "STOCK_TRANSFER_IN"
transferFunctionCodeOut = "STOCK_TRANSFER_OUT"
)
type transferRoutePair struct {
Stockable commonSvc.FifoStockV2RouteRule
Usable commonSvc.FifoStockV2RouteRule
}
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, fifoStockV2Svc commonSvc.FifoStockV2Service, expenseBridge TransferExpenseBridge) TransferService {
return &transferService{
Log: utils.Log,
@@ -444,9 +454,9 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
}
}
pakanProducts := map[uint]bool{}
if s.FifoStockV2Svc != nil && len(req.Products) > 0 {
pakanProducts, err = s.resolvePakanProducts(c.Context(), tx, req.Products)
routePairsByProductID := map[uint]transferRoutePair{}
if len(req.Products) > 0 {
routePairsByProductID, err = s.resolveTransferRoutes(c.Context(), tx, req.Products)
if err != nil {
return err
}
@@ -454,10 +464,17 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
for _, product := range req.Products {
detail := detailMap[uint64(product.ProductID)]
routePair, ok := routePairsByProductID[uint(product.ProductID)]
if !ok {
return fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Konfigurasi FIFO v2 transfer tidak ditemukan untuk produk %d", product.ProductID),
)
}
outUsageQty := 0.0
outPendingQty := 0.0
useFifoV2 := s.FifoStockV2Svc != nil && pakanProducts[uint(product.ProductID)]
useFifoV2 := s.FifoStockV2Svc != nil
if useFifoV2 {
s.Log.Infof(
"[fifo-v2][transfer] use reflow movement=%s detail_id=%d product_id=%d source_pw=%d qty=%.3f",
@@ -468,12 +485,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
product.ProductQty,
)
reflowResult, err := s.FifoStockV2Svc.Reflow(c.Context(), commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: "PAKAN",
FlagGroupCode: routePair.Usable.FlagGroupCode,
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
Usable: commonSvc.FifoStockV2Ref{
ID: uint(detail.Id),
LegacyTypeKey: fifo.UsableKeyStockTransferOut.String(),
FunctionCode: "STOCK_TRANSFER_OUT",
LegacyTypeKey: routePair.Usable.LegacyTypeKey,
FunctionCode: routePair.Usable.FunctionCode,
},
DesiredQty: product.ProductQty,
Tx: tx,
@@ -491,8 +508,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
outPendingQty,
)
} else {
usableKey := fifo.UsableKey(strings.TrimSpace(routePair.Usable.LegacyTypeKey))
if usableKey == "" {
usableKey = fifo.UsableKeyStockTransferOut
}
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
UsableKey: fifo.UsableKeyStockTransferOut,
UsableKey: usableKey,
UsableID: uint(detail.Id),
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
Quantity: product.ProductQty,
@@ -553,8 +574,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
product.ProductQty,
)
}
stockableKey := fifo.StockableKey(strings.TrimSpace(routePair.Stockable.LegacyTypeKey))
if stockableKey == "" {
stockableKey = fifo.StockableKeyStockTransferIn
}
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
StockableKey: fifo.StockableKeyStockTransferIn,
StockableKey: stockableKey,
StockableID: uint(detail.Id),
ProductWarehouseID: uint(*detail.DestProductWarehouseID),
Quantity: product.ProductQty,
@@ -657,50 +682,72 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
return result, nil
}
func (s *transferService) resolvePakanProducts(
func (s *transferService) resolveTransferRoutes(
ctx context.Context,
tx *gorm.DB,
products []validation.TransferProduct,
) (map[uint]bool, error) {
out := make(map[uint]bool, len(products))
) (map[uint]transferRoutePair, error) {
out := make(map[uint]transferRoutePair, len(products))
if len(products) == 0 {
return out, nil
}
productIDs := make([]uint, 0, len(products))
seen := make(map[uint]struct{}, len(products))
productIDs := make(map[uint]struct{}, len(products))
for _, product := range products {
if product.ProductID == 0 {
continue
}
if _, ok := seen[product.ProductID]; ok {
continue
productIDs[product.ProductID] = struct{}{}
}
for productID := range productIDs {
usableRoute, err := commonSvc.ResolveFifoStockV2RouteByProductIDAndLane(
ctx,
tx,
productID,
transferFunctionCodeOut,
commonSvc.FifoStockV2LaneUsable,
)
if err != nil {
return nil, err
}
if usableRoute == nil {
return nil, fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Produk %d tidak mendukung transaksi Transfer Stock (OUT) pada matrix FIFO v2", productID),
)
}
stockableRoute, err := commonSvc.ResolveFifoStockV2RouteByProductIDAndLane(
ctx,
tx,
productID,
transferFunctionCodeIn,
commonSvc.FifoStockV2LaneStockable,
)
if err != nil {
return nil, err
}
if stockableRoute == nil {
return nil, fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Produk %d tidak mendukung transaksi Transfer Stock (IN) pada matrix FIFO v2", productID),
)
}
if strings.TrimSpace(usableRoute.FlagGroupCode) != strings.TrimSpace(stockableRoute.FlagGroupCode) {
return nil, fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Konfigurasi matrix FIFO v2 transfer tidak konsisten untuk produk %d", productID),
)
}
out[productID] = transferRoutePair{
Stockable: *stockableRoute,
Usable: *usableRoute,
}
seen[product.ProductID] = struct{}{}
productIDs = append(productIDs, product.ProductID)
}
if len(productIDs) == 0 {
return out, nil
}
type row struct {
ProductID uint `gorm:"column:product_id"`
}
var rows []row
err := tx.WithContext(ctx).
Table("flags f").
Select("DISTINCT f.flagable_id AS product_id").
Where("f.flagable_type = ?", entity.FlagableTypeProduct).
Where("f.name IN ?", []string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"}).
Where("f.flagable_id IN ?", productIDs).
Scan(&rows).Error
if err != nil {
return nil, err
}
for _, row := range rows {
out[row.ProductID] = true
}
return out, nil
}