mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
FIX[BE]: fixing bug transfer to laying, delet biaya, nominal expesen e, chickin
This commit is contained in:
+9
@@ -0,0 +1,9 @@
|
||||
-- Drop foreign key and column
|
||||
ALTER TABLE laying_transfers
|
||||
DROP CONSTRAINT IF EXISTS fk_laying_transfers_product_warehouse_id;
|
||||
|
||||
ALTER TABLE laying_transfers
|
||||
DROP COLUMN IF EXISTS product_warehouse_id;
|
||||
|
||||
-- Drop index
|
||||
DROP INDEX IF EXISTS idx_laying_transfers_product_warehouse_id;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
-- Add product_warehouse_id to laying_transfers for FIFO support
|
||||
ALTER TABLE laying_transfers
|
||||
ADD COLUMN product_warehouse_id BIGINT;
|
||||
|
||||
-- Add foreign key
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||
ALTER TABLE laying_transfers
|
||||
ADD CONSTRAINT fk_laying_transfers_product_warehouse_id
|
||||
FOREIGN KEY (product_warehouse_id)
|
||||
REFERENCES product_warehouses(id)
|
||||
ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add index
|
||||
CREATE INDEX idx_laying_transfers_product_warehouse_id
|
||||
ON laying_transfers(product_warehouse_id);
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
-- Rollback: Remove STOCKABLE fields from laying_transfers
|
||||
|
||||
-- Drop index
|
||||
DROP INDEX IF EXISTS idx_laying_transfers_dest_product_warehouse_id;
|
||||
|
||||
-- Drop foreign key constraint
|
||||
ALTER TABLE laying_transfers
|
||||
DROP CONSTRAINT IF EXISTS fk_laying_transfers_dest_product_warehouse_id;
|
||||
|
||||
-- Drop columns
|
||||
ALTER TABLE laying_transfers
|
||||
DROP COLUMN IF EXISTS dest_product_warehouse_id,
|
||||
DROP COLUMN IF EXISTS total_qty,
|
||||
DROP COLUMN IF EXISTS total_used;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
-- Add STOCKABLE fields to laying_transfers for destination warehouse
|
||||
-- This enables Transfer to Laying to work as DUAL ROLE (Stockable + Usable)
|
||||
|
||||
-- Add columns for STOCKABLE role (destination warehouse)
|
||||
ALTER TABLE laying_transfers
|
||||
ADD COLUMN dest_product_warehouse_id BIGINT,
|
||||
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0,
|
||||
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0;
|
||||
|
||||
-- Add foreign key constraint
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||
ALTER TABLE laying_transfers
|
||||
ADD CONSTRAINT fk_laying_transfers_dest_product_warehouse_id
|
||||
FOREIGN KEY (dest_product_warehouse_id)
|
||||
REFERENCES product_warehouses(id)
|
||||
ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add index for performance
|
||||
CREATE INDEX idx_laying_transfers_dest_product_warehouse_id
|
||||
ON laying_transfers(dest_product_warehouse_id);
|
||||
|
||||
-- Add comment for documentation
|
||||
COMMENT ON COLUMN laying_transfers.product_warehouse_id IS 'Product warehouse at source (Growing flock) - for USABLE role';
|
||||
COMMENT ON COLUMN laying_transfers.dest_product_warehouse_id IS 'Product warehouse at destination (Laying flock) - for STOCKABLE role';
|
||||
COMMENT ON COLUMN laying_transfers.total_qty IS 'Total lot quantity introduced to destination warehouse - for STOCKABLE role';
|
||||
COMMENT ON COLUMN laying_transfers.total_used IS 'Quantity already consumed from this lot at destination - for STOCKABLE role';
|
||||
@@ -12,18 +12,29 @@ type LayingTransfer struct {
|
||||
FromProjectFlockId uint `gorm:"not null"`
|
||||
ToProjectFlockId uint `gorm:"not null"`
|
||||
TransferDate time.Time `gorm:"type:date;not null"`
|
||||
|
||||
|
||||
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
|
||||
UsageQty *float64 `gorm:"type:numeric(15,3)"`
|
||||
ProductWarehouseId *uint `gorm:"type:bigint"` // Source PW (PULLET)
|
||||
|
||||
|
||||
DestProductWarehouseID *uint `gorm:"column:dest_product_warehouse_id;type:bigint"` // Destination PW (LAYER)
|
||||
TotalQty float64 `gorm:"column:total_qty;type:numeric(15,3);default:0"` // Total lot introduced to destination
|
||||
TotalUsed float64 `gorm:"column:total_used;type:numeric(15,3);default:0"` // Already consumed from this lot
|
||||
|
||||
Notes string `gorm:"type:text"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
|
||||
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
|
||||
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
|
||||
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
|
||||
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` // Source PW
|
||||
DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID;references:Id"` // Destination PW
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
|
||||
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||
|
||||
err := fifoService.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKey("ADJUSTMENT_IN"),
|
||||
Key: fifo.StockableKeyAdjustmentIn,
|
||||
Table: "adjustment_stocks",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
@@ -52,7 +52,7 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
|
||||
}
|
||||
|
||||
err = fifoService.RegisterUsable(fifo.UsableConfig{
|
||||
Key: fifo.UsableKey("ADJUSTMENT_OUT"),
|
||||
Key: fifo.UsableKeyAdjustmentOut,
|
||||
Table: "adjustment_stocks",
|
||||
Columns: fifo.UsableColumns{
|
||||
ID: "id",
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -123,15 +124,9 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
projectFlockKandangID = &pfkID
|
||||
}
|
||||
|
||||
pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(
|
||||
ctx,
|
||||
uint(req.ProductID),
|
||||
uint(req.WarehouseID),
|
||||
projectFlockKandangID,
|
||||
)
|
||||
pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(ctx, uint(req.ProductID), uint(req.WarehouseID), projectFlockKandangID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to find product warehouse: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||
}
|
||||
|
||||
@@ -143,7 +138,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
}
|
||||
|
||||
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create product warehouse: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse")
|
||||
}
|
||||
pw = newPW
|
||||
@@ -163,7 +157,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||
}
|
||||
|
||||
// Create StockLog for history tracking
|
||||
afterQuantity := productWarehouse.Quantity
|
||||
newLog := &entity.StockLog{
|
||||
LoggableType: string(utils.StockLogTypeAdjustment),
|
||||
@@ -189,7 +182,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
return err
|
||||
}
|
||||
|
||||
// Create AdjustmentStock record for FIFO tracking
|
||||
adjustmentStock := &entity.AdjustmentStock{
|
||||
StockLogId: newLog.Id,
|
||||
ProductWarehouseId: productWarehouse.Id,
|
||||
@@ -200,10 +192,10 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
}
|
||||
|
||||
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
||||
// Adjustment INCREASE → Replenish stock (Stockable)
|
||||
|
||||
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
|
||||
_, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
|
||||
StockableKey: "ADJUSTMENT_IN",
|
||||
StockableKey: fifo.StockableKeyAdjustmentIn,
|
||||
StockableID: adjustmentStock.Id,
|
||||
ProductWarehouseID: uint(productWarehouse.Id),
|
||||
Quantity: req.Quantity,
|
||||
@@ -215,9 +207,8 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
}
|
||||
|
||||
} else {
|
||||
// Adjustment DECREASE → Consume stock (Usable)
|
||||
_, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
|
||||
UsableKey: "ADJUSTMENT_OUT",
|
||||
UsableKey: fifo.UsableKeyAdjustmentOut,
|
||||
UsableID: adjustmentStock.Id,
|
||||
ProductWarehouseID: uint(productWarehouse.Id),
|
||||
Quantity: req.Quantity,
|
||||
@@ -230,6 +221,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
}
|
||||
|
||||
// Update ProductWarehouse quantity (for backward compatibility/reporting)
|
||||
|
||||
productWarehouse.Quantity = afterQuantity
|
||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil {
|
||||
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
|
||||
|
||||
@@ -43,12 +43,10 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Initialize FIFO Service
|
||||
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||
|
||||
// Register Transfer as Stockable (adds stock to destination warehouse)
|
||||
err = fifoService.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKey("STOCK_TRANSFER_IN"),
|
||||
Key: fifo.StockableKeyStockTransferIn,
|
||||
Table: "stock_transfer_details",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
@@ -63,9 +61,8 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Register Transfer as Usable (consumes stock from source warehouse)
|
||||
err = fifoService.RegisterUsable(fifo.UsableConfig{
|
||||
Key: fifo.UsableKey("STOCK_TRANSFER_OUT"),
|
||||
Key: fifo.UsableKeyStockTransferOut,
|
||||
Table: "stock_transfer_details",
|
||||
Columns: fifo.UsableColumns{
|
||||
ID: "id",
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -337,24 +338,21 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
}
|
||||
|
||||
// Execute FIFO operations for each product
|
||||
for _, product := range req.Products {
|
||||
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",
|
||||
UsableKey: fifo.UsableKeyStockTransferOut,
|
||||
UsableID: uint(detail.Id),
|
||||
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
|
||||
Quantity: product.ProductQty,
|
||||
AllowPending: false, // Don't allow pending, must have actual stock
|
||||
AllowPending: false,
|
||||
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))
|
||||
}
|
||||
|
||||
// Update usage tracking fields for source warehouse
|
||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||
Where("id = ?", detail.Id).
|
||||
Updates(map[string]interface{}{
|
||||
@@ -367,7 +365,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
// 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",
|
||||
StockableKey: fifo.StockableKeyStockTransferIn,
|
||||
StockableID: uint(detail.Id),
|
||||
ProductWarehouseID: uint(*detail.DestProductWarehouseID),
|
||||
Quantity: product.ProductQty,
|
||||
@@ -378,7 +376,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok di gudang tujuan untuk produk %d: %v", product.ProductID, err))
|
||||
}
|
||||
|
||||
// Update total tracking fields for destination warehouse
|
||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||
Where("id = ?", detail.Id).
|
||||
Updates(map[string]interface{}{
|
||||
|
||||
@@ -58,6 +58,24 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if err := fifoService.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKeyProjectFlockPopulation,
|
||||
Table: "project_flock_populations",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
TotalQuantity: "total_qty",
|
||||
TotalUsedQuantity: "total_used_qty",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
OrderBy: []string{"created_at ASC", "id ASC"},
|
||||
}); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
panic(fmt.Sprintf("failed to register project flock population stockable workflow: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil {
|
||||
|
||||
@@ -17,6 +17,7 @@ type ProjectChickinRepository interface {
|
||||
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||
GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error)
|
||||
GetByProjectFlockKandangIDForUpdate(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
||||
UpdateUsageFields(ctx context.Context, tx *gorm.DB, chickinID uint, usageQty, pendingUsageQty float64) error
|
||||
}
|
||||
|
||||
type ChickinRepositoryImpl struct {
|
||||
@@ -123,3 +124,13 @@ func (r *ChickinRepositoryImpl) GetTotalChickinQtyByProjectFlockID(ctx context.C
|
||||
Scan(&result).Error
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (r *ChickinRepositoryImpl) UpdateUsageFields(ctx context.Context, tx *gorm.DB, chickinID uint, usageQty, pendingUsageQty float64) error {
|
||||
return tx.WithContext(ctx).
|
||||
Model(&entity.ProjectChickin{}).
|
||||
Where("id = ?", chickinID).
|
||||
Updates(map[string]interface{}{
|
||||
"usage_qty": usageQty,
|
||||
"pending_usage_qty": pendingUsageQty,
|
||||
}).Error
|
||||
}
|
||||
|
||||
@@ -214,9 +214,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
pendingQtyMap[existingChickin.ProductWarehouseId] += existingChickin.PendingUsageQty
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL: Validate chickins sequentially to prevent over-allocation within the same request
|
||||
// pendingQtyMap is accumulated as we validate each chickin to ensure total pending doesn't exceed available stock
|
||||
for idx, chickin := range newChikins {
|
||||
pendingQty := pendingQtyMap[chickin.ProductWarehouseId]
|
||||
desiredQty := chickinQtyMap[uint(idx)]
|
||||
@@ -232,8 +229,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
|
||||
chickinQtyMap[uint(idx)] = availableQty
|
||||
|
||||
// ACCUMULATE pending for this product warehouse so NEXT chickin in same request sees it
|
||||
// This prevents multiple chickins in same request from over-allocating the same stock
|
||||
pendingQtyMap[chickin.ProductWarehouseId] += availableQty
|
||||
}
|
||||
|
||||
@@ -358,12 +353,15 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
}
|
||||
|
||||
if chickin.UsageQty > 0 {
|
||||
|
||||
currentUsageQty := chickin.UsageQty
|
||||
|
||||
if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
warehouseDeltas := make(map[uint]float64)
|
||||
warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty
|
||||
warehouseDeltas[chickin.ProductWarehouseId] += currentUsageQty
|
||||
if err := s.adjustProductWarehouseQuantities(c.Context(), s.Repository.DB(), warehouseDeltas); err != nil {
|
||||
s.Log.Errorf("Failed to adjust product warehouses for deleted chickin %d: %+v", chickin.Id, err)
|
||||
return err
|
||||
@@ -618,7 +616,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
||||
population := &entity.ProjectFlockPopulation{
|
||||
ProjectChickinId: chickin.Id,
|
||||
ProductWarehouseId: targetPW.Id,
|
||||
TotalQty: quantityToConvert,
|
||||
TotalQty: 0, // Will be set by FIFO Replenish
|
||||
TotalUsedQty: 0,
|
||||
Notes: chickin.Notes,
|
||||
CreatedBy: actorID,
|
||||
@@ -634,15 +632,22 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
||||
return fmt.Errorf("failed to reset pending usage qty for chickin %d: %w", chickin.Id, err)
|
||||
}
|
||||
|
||||
// Replenish stock to target ProductWarehouse based on source flag
|
||||
// StockableKey is PROJECT_CHICKIN but StockableID refers to Population ID
|
||||
if err := s.ReplenishChickinStocks(ctx.Context(), dbTransaction, &chickin, targetPW, population, actorID); err != nil {
|
||||
s.Log.Errorf("Failed to replenish stock for chickin %d: %+v", chickin.Id, err)
|
||||
return err
|
||||
}
|
||||
|
||||
totalQuantityAdded += quantityToConvert
|
||||
}
|
||||
|
||||
// NOTE: Tidak menambah target ProductWarehouse quantity karena:
|
||||
// 1. Ayam sudah dipakai (masuk population)
|
||||
// 2. ProductWarehouse source sudah berkurang saat create chickin (ConsumeChickinStocks)
|
||||
// 3. Menambah quantity disini akan menyebabkan double count
|
||||
//
|
||||
// PULLET/LAYER untuk flock ini akan di-add lewat mekanisme lain (misal: purchase, transfer, dll)
|
||||
// NOTE: ProductWarehouse target sudah ditambah melalui ReplenishChickinStocks
|
||||
// yang dipanggil di atas untuk setiap chickin berdasarkan flag source:
|
||||
// - DOC → replenish ke PULLET
|
||||
// - PULLET → replenish ke LAYER
|
||||
// - LAYER → tidak perlu replenish (sudah final)
|
||||
// - DOC+PULLET+LAYER → replenish ke dirinya sendiri
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -671,10 +676,7 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
|
||||
s.Log.Infof("ConsumeChickinStocks result: usage_qty=%.3f, pending_qty=%.3f, allocated_allocations=%d",
|
||||
result.UsageQuantity, result.PendingQuantity, len(result.AddedAllocations))
|
||||
|
||||
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Updates(map[string]interface{}{
|
||||
"usage_qty": result.UsageQuantity,
|
||||
"pending_usage_qty": result.PendingQuantity,
|
||||
}).Error; err != nil {
|
||||
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -696,6 +698,101 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, targetPW *entity.ProductWarehouse, population *entity.ProjectFlockPopulation, actorID uint) error {
|
||||
if chickin == nil || targetPW == nil || population == nil || s.FifoSvc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
sourcePW, err := s.ProductWarehouseRepo.GetByID(ctx, chickin.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Product.Flags")
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
}
|
||||
if sourcePW == nil || sourcePW.Product.Id == 0 {
|
||||
return fmt.Errorf("source product warehouse or product not found for chickin %d", chickin.Id)
|
||||
}
|
||||
|
||||
sourceFlags := sourcePW.Product.Flags
|
||||
if len(sourceFlags) == 0 {
|
||||
s.Log.Warnf("Source product %d has no flags, skipping replenish for chickin %d", sourcePW.Product.Id, chickin.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
hasDoc := false
|
||||
hasPullet := false
|
||||
hasLayer := false
|
||||
for _, flag := range sourceFlags {
|
||||
flagName := utils.FlagType(flag.Name)
|
||||
if flagName == utils.FlagDOC {
|
||||
hasDoc = true
|
||||
} else if flagName == utils.FlagPullet {
|
||||
hasPullet = true
|
||||
} else if flagName == utils.FlagLayer {
|
||||
hasLayer = true
|
||||
}
|
||||
}
|
||||
|
||||
if hasDoc && hasPullet && hasLayer {
|
||||
s.Log.Infof("Chickin %d has mixed flags (DOC+PULLET+LAYER), replenishing to source PW %d", chickin.Id, sourcePW.Id)
|
||||
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
||||
StockableID: population.Id,
|
||||
ProductWarehouseID: sourcePW.Id,
|
||||
Quantity: chickin.UsageQty,
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to replenish stock to source PW for chickin %d: %+v", chickin.Id, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LAYER only - no replenish needed
|
||||
if hasLayer && !hasDoc && !hasPullet {
|
||||
s.Log.Infof("Chickin %d has LAYER flag only, skipping replenish", chickin.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
if hasDoc && !hasPullet && !hasLayer {
|
||||
s.Log.Infof("Chickin %d has DOC flag, replenishing to PULLET PW %d", chickin.Id, targetPW.Id)
|
||||
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
||||
StockableID: population.Id,
|
||||
ProductWarehouseID: targetPW.Id,
|
||||
Quantity: chickin.UsageQty,
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to replenish stock to PULLET PW for chickin %d: %+v", chickin.Id, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if hasPullet && !hasDoc && !hasLayer {
|
||||
s.Log.Infof("Chickin %d has PULLET flag, replenishing to LAYER PW %d", chickin.Id, targetPW.Id)
|
||||
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
||||
StockableID: population.Id,
|
||||
ProductWarehouseID: targetPW.Id,
|
||||
Quantity: chickin.UsageQty,
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to replenish stock to LAYER PW for chickin %d: %+v", chickin.Id, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Other combinations (e.g., DOC + PULLET without LAYER) - skip for now
|
||||
s.Log.Warnf("Chickin %d has unsupported flag combination, skipping replenish", chickin.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, actorID uint) error {
|
||||
if chickin == nil || s.FifoSvc == nil {
|
||||
return nil
|
||||
@@ -703,8 +800,7 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
||||
|
||||
var currentUsage float64
|
||||
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(¤tUsage).Error; err != nil {
|
||||
s.Log.Warnf("Failed to get current usage for chickin %d: %+v", chickin.Id, err)
|
||||
currentUsage = 0
|
||||
|
||||
}
|
||||
|
||||
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
|
||||
@@ -716,14 +812,10 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Updates(map[string]interface{}{
|
||||
"usage_qty": 0,
|
||||
"pending_usage_qty": 0,
|
||||
}).Error; err != nil {
|
||||
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create stock log for the restoration
|
||||
if currentUsage > 0 {
|
||||
increaseLog := &entity.StockLog{
|
||||
Increase: currentUsage,
|
||||
@@ -734,8 +826,7 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
||||
Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id),
|
||||
}
|
||||
if err := s.StockLogRepo.CreateOne(ctx, increaseLog, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock log for chickin %d: %+v", chickin.Id, err)
|
||||
// Don't return error here, stock already released
|
||||
s.Log.Errorf("Failed to create stock log for released chickin %d: %+v", chickin.Id, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+23
-2
@@ -552,6 +552,7 @@ func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validati
|
||||
}
|
||||
|
||||
func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) {
|
||||
|
||||
availableQty := productWarehouse.Quantity
|
||||
|
||||
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
|
||||
@@ -564,7 +565,17 @@ func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehous
|
||||
}
|
||||
}
|
||||
|
||||
availableQty = productWarehouse.Quantity - totalPendingQty
|
||||
totalPopulationQty := 0.0
|
||||
if s.PopulationRepo != nil {
|
||||
popQty, err := s.PopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), productWarehouse.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get population qty for PW %d: %+v", productWarehouse.Id, err)
|
||||
} else {
|
||||
totalPopulationQty = popQty
|
||||
}
|
||||
}
|
||||
|
||||
availableQty = productWarehouse.Quantity - totalPendingQty - totalPopulationQty
|
||||
if availableQty < 0 {
|
||||
availableQty = 0
|
||||
}
|
||||
@@ -578,7 +589,17 @@ func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehous
|
||||
}
|
||||
}
|
||||
|
||||
availableQty = productWarehouse.Quantity - totalPendingQty
|
||||
totalPopulationQty := 0.0
|
||||
if s.PopulationRepo != nil {
|
||||
popQty, err := s.PopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), productWarehouse.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get population qty for PW %d: %+v", productWarehouse.Id, err)
|
||||
} else {
|
||||
totalPopulationQty = popQty
|
||||
}
|
||||
}
|
||||
|
||||
availableQty = productWarehouse.Quantity - totalPendingQty - totalPopulationQty
|
||||
if availableQty < 0 {
|
||||
availableQty = 0
|
||||
}
|
||||
|
||||
+14
-3
@@ -9,19 +9,17 @@ import (
|
||||
)
|
||||
|
||||
type ProjectFlockPopulationRepository interface {
|
||||
// domain-specific
|
||||
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error)
|
||||
ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error)
|
||||
GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
|
||||
GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
|
||||
GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||
GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
|
||||
GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||
|
||||
// subset of base repository methods used by services
|
||||
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
|
||||
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
|
||||
|
||||
// transaction helpers
|
||||
WithTx(tx *gorm.DB) ProjectFlockPopulationRepository
|
||||
DB() *gorm.DB
|
||||
}
|
||||
@@ -108,6 +106,19 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangI
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error) {
|
||||
var total float64
|
||||
err := r.DB().WithContext(ctx).
|
||||
Model(&entity.ProjectFlockPopulation{}).
|
||||
Where("product_warehouse_id = ?", productWarehouseID).
|
||||
Select("COALESCE(SUM(total_qty), 0)").
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
|
||||
var total float64
|
||||
err := r.DB().WithContext(ctx).
|
||||
|
||||
+14
@@ -28,6 +28,7 @@ type ProjectFlockKandangRepository interface {
|
||||
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
|
||||
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
|
||||
ListIDsByProjectAndKandang(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
|
||||
GetTotalPendingChickinQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
|
||||
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
||||
DB() *gorm.DB
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
@@ -206,6 +207,19 @@ func (r *projectFlockKandangRepositoryImpl) IdExists(ctx context.Context, id uin
|
||||
return repository.Exists[entity.ProjectFlockKandang](ctx, r.db, id)
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) GetTotalPendingChickinQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error) {
|
||||
var total float64
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("project_chickins").
|
||||
Select("COALESCE(SUM(pending_usage_qty), 0)").
|
||||
Where("product_warehouse_id = ?", productWarehouseID).
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) {
|
||||
record := new(entity.ProjectFlockKandang)
|
||||
if err := r.db.WithContext(ctx).
|
||||
|
||||
@@ -2,6 +2,7 @@ package transfer_layings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
||||
sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
|
||||
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
@@ -31,6 +33,44 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
|
||||
productWarehouseRepo := rInventory.NewProductWarehouseRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
|
||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
||||
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
||||
|
||||
|
||||
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
||||
Key: fifo.UsableKeyTransferToLaying,
|
||||
Table: "laying_transfers",
|
||||
Columns: fifo.UsableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
UsageQuantity: "usage_qty",
|
||||
PendingQuantity: "pending_usage_qty",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
}); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
panic(fmt.Sprintf("failed to register transfer to laying usable workflow: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if err := fifoService.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKeyTransferToLaying,
|
||||
Table: "laying_transfers",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "dest_product_warehouse_id",
|
||||
TotalQuantity: "total_qty",
|
||||
TotalUsedQuantity: "total_used",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
OrderBy: []string{"created_at ASC", "id ASC"},
|
||||
}); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
panic(fmt.Sprintf("failed to register transfer to laying stockable workflow: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowTransferToLaying, utils.TransferToLayingApprovalSteps); err != nil {
|
||||
@@ -45,6 +85,7 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
|
||||
productWarehouseRepo,
|
||||
warehouseRepo,
|
||||
approvalService,
|
||||
fifoService,
|
||||
validate,
|
||||
)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -45,6 +46,7 @@ type transferLayingService struct {
|
||||
ProductWarehouseRepo rInventory.ProductWarehouseRepository
|
||||
WarehouseRepo rWarehouse.WarehouseRepository
|
||||
ApprovalService commonSvc.ApprovalService
|
||||
FifoSvc commonSvc.FifoService
|
||||
}
|
||||
|
||||
func NewTransferLayingService(
|
||||
@@ -55,6 +57,7 @@ func NewTransferLayingService(
|
||||
productWarehouseRepo rInventory.ProductWarehouseRepository,
|
||||
warehouseRepo rWarehouse.WarehouseRepository,
|
||||
approvalService commonSvc.ApprovalService,
|
||||
fifoSvc commonSvc.FifoService,
|
||||
validate *validator.Validate,
|
||||
) TransferLayingService {
|
||||
return &transferLayingService{
|
||||
@@ -67,6 +70,7 @@ func NewTransferLayingService(
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ApprovalService: approvalService,
|
||||
FifoSvc: fifoSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,15 +272,20 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
if len(sourceWarehouseMap) > 0 {
|
||||
for _, pwID := range sourceWarehouseMap {
|
||||
createBody.ProductWarehouseId = &pwID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Repository.WithTx(dbTransaction).CreateOne(c.Context(), createBody, nil); err != nil {
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
repoTx := s.Repository.WithTx(dbTransaction)
|
||||
|
||||
if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying record")
|
||||
}
|
||||
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
||||
|
||||
for _, sourceDetail := range req.SourceKandangs {
|
||||
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]
|
||||
|
||||
@@ -290,13 +299,6 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source")
|
||||
}
|
||||
|
||||
if err := s.reduceProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, sourceDetail.ProjectFlockKandangId, sourceDetail.Quantity); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reduce project flock population")
|
||||
}
|
||||
|
||||
if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"qty": gorm.Expr("qty - ?", sourceDetail.Quantity)}, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update source warehouse quantity")
|
||||
}
|
||||
}
|
||||
|
||||
for _, targetDetail := range req.TargetKandangs {
|
||||
@@ -325,6 +327,22 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
}
|
||||
}
|
||||
|
||||
// Set DestProductWarehouseID untuk STOCKABLE role (ambil dari target pertama)
|
||||
if len(req.TargetKandangs) > 0 {
|
||||
firstTargetPWID := req.TargetKandangs[0].ProjectFlockKandangId
|
||||
// Cari ProductWarehouse untuk target kandang
|
||||
targetWarehouse, _ := s.WarehouseRepo.GetLatestByKandangID(c.Context(), firstTargetPWID)
|
||||
if targetWarehouse != nil {
|
||||
// Query ProductWarehouse by warehouse and kandang
|
||||
var targetPW entity.ProductWarehouse
|
||||
err := dbTransaction.Where("warehouse_id = ? AND project_flock_kandang_id = ?", targetWarehouse.Id, firstTargetPWID).
|
||||
First(&targetPW).Error
|
||||
if err == nil {
|
||||
createBody.DestProductWarehouseID = &targetPW.Id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := createApprovalTransferLaying(c.Context(), dbTransaction, createBody.Id, createBody.CreatedBy); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer approval")
|
||||
}
|
||||
@@ -339,7 +357,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
return s.GetOne(c, createBody.Id)
|
||||
}
|
||||
|
||||
func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) {
|
||||
func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -381,6 +399,7 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
||||
}
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
repoTx := s.Repository.WithTx(dbTransaction)
|
||||
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||
|
||||
@@ -416,7 +435,7 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
||||
totalSourceQty += source.Quantity
|
||||
}
|
||||
|
||||
if err := s.Repository.WithTx(dbTransaction).PatchOne(c.Context(), id, map[string]any{
|
||||
if err := repoTx.PatchOne(c.Context(), id, map[string]any{
|
||||
"transfer_date": transferDate,
|
||||
"notes": req.Reason,
|
||||
"pending_usage_qty": &totalSourceQty,
|
||||
@@ -531,8 +550,9 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
}
|
||||
}
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
|
||||
repoTx := s.Repository.WithTx(dbTransaction)
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
||||
|
||||
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
||||
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), id)
|
||||
@@ -551,7 +571,6 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
}
|
||||
}
|
||||
|
||||
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
||||
for _, source := range sources {
|
||||
populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -575,7 +594,7 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Repository.WithTx(dbTransaction).DeleteOne(c.Context(), id); err != nil {
|
||||
if err := repoTx.DeleteOne(c.Context(), id); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
|
||||
}
|
||||
|
||||
@@ -624,14 +643,13 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
||||
}
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
|
||||
repoTx := s.Repository.WithTx(dbTransaction)
|
||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
||||
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||
|
||||
for _, approvableID := range approvableIDs {
|
||||
transfer, err := s.Repository.GetByID(c.Context(), approvableID, nil)
|
||||
transfer, err := repoTx.GetByID(c.Context(), approvableID, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("TransferLaying %d not found", approvableID))
|
||||
@@ -664,44 +682,45 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
||||
}
|
||||
|
||||
if len(sources) > 0 && len(targets) > 0 {
|
||||
firstSource := sources[0]
|
||||
if firstSource.ProductWarehouseId == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID))
|
||||
}
|
||||
|
||||
sourceWarehouse, err := productWarehouseRepoTx.GetByID(c.Context(), *firstSource.ProductWarehouseId, nil)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source warehouse")
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
|
||||
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), target.TargetProjectFlockKandangId)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
continue
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
|
||||
for _, source := range sources {
|
||||
if source.ProductWarehouseId == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID))
|
||||
}
|
||||
|
||||
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId)
|
||||
_, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
|
||||
UsableKey: fifo.UsableKeyTransferToLaying,
|
||||
UsableID: approvableID,
|
||||
ProductWarehouseID: *source.ProductWarehouseId,
|
||||
Quantity: source.Qty,
|
||||
AllowPending: false,
|
||||
Tx: dbTransaction,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
continue
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to consume FIFO stock for source %d: %v", source.ProductWarehouseId, err))
|
||||
}
|
||||
}
|
||||
|
||||
if transfer.DestProductWarehouseID != nil {
|
||||
note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber)
|
||||
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||
StockableKey: fifo.StockableKeyTransferToLaying,
|
||||
StockableID: approvableID,
|
||||
ProductWarehouseID: *transfer.DestProductWarehouseID,
|
||||
Quantity: *transfer.PendingUsageQty,
|
||||
Note: ¬e,
|
||||
Tx: dbTransaction,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock to destination warehouse: %v", err))
|
||||
}
|
||||
|
||||
if _, err := s.getOrCreateProductWarehouse(
|
||||
c.Context(),
|
||||
dbTransaction,
|
||||
sourceWarehouse.ProductId,
|
||||
targetWarehouse.Id,
|
||||
target.Qty,
|
||||
actorID,
|
||||
&target.TargetProjectFlockKandangId, // Set flock ID agar bisa di-chickin di target flock
|
||||
); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create or update product warehouse")
|
||||
if err := dbTransaction.Model(&entity.LayingTransfer{}).
|
||||
Where("id = ?", approvableID).
|
||||
Updates(map[string]interface{}{
|
||||
"total_qty": replenishResult.AddedQuantity,
|
||||
}).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update total quantity for transfer")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -709,9 +728,10 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
||||
usageQty := *transfer.PendingUsageQty
|
||||
updateData := map[string]any{
|
||||
"usage_qty": usageQty,
|
||||
"total_qty": usageQty, // Same as usage_qty for initial transfer
|
||||
"pending_usage_qty": nil,
|
||||
}
|
||||
if err := s.Repository.WithTx(dbTransaction).PatchOne(c.Context(), approvableID, updateData, nil); err != nil {
|
||||
if err := repoTx.PatchOne(c.Context(), approvableID, updateData, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying status")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
|
||||
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||
_ = fifoService.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKey("PURCHASE_ITEMS"),
|
||||
Key: fifo.StockableKeyPurchaseItems,
|
||||
Table: "purchase_items",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
|
||||
@@ -42,8 +42,7 @@ type PurchaseService interface {
|
||||
}
|
||||
|
||||
const (
|
||||
priceTolerance = 0.0001
|
||||
purchaseStockableKey = fifo.StockableKey("PURCHASE_ITEMS")
|
||||
priceTolerance = 0.0001
|
||||
)
|
||||
|
||||
type purchaseService struct {
|
||||
@@ -924,7 +923,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
continue
|
||||
}
|
||||
if _, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||
StockableKey: purchaseStockableKey,
|
||||
StockableKey: fifo.StockableKeyPurchaseItems,
|
||||
StockableID: adj.itemID,
|
||||
ProductWarehouseID: adj.pwID,
|
||||
Quantity: adj.qty,
|
||||
|
||||
@@ -106,7 +106,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
Where("r.deleted_at IS NULL")
|
||||
recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs)
|
||||
|
||||
purchaseStockableKey := fifo.StockableKey("PURCHASE_ITEMS").String()
|
||||
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
|
||||
transferStockableKey := fifo.StockableKey("STOCK_TRANSFER_DETAILS").String()
|
||||
|
||||
query := r.db.WithContext(ctx).
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
package fifo
|
||||
|
||||
const (
|
||||
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
||||
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
|
||||
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
|
||||
// Usable Keys
|
||||
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
||||
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
|
||||
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
|
||||
UsableKeyTransferToLaying UsableKey = "TRANSFER_TO_LAYING"
|
||||
UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT"
|
||||
UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT"
|
||||
|
||||
// Stockable Keys
|
||||
StockableKeyTransferToLaying StockableKey = "TRANSFER_TO_LAYING"
|
||||
StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN"
|
||||
StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN"
|
||||
StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS"
|
||||
StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user