mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
fix: chickin include stock allocation, fix calculation hpp
This commit is contained in:
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
@@ -19,6 +21,7 @@ import (
|
||||
rProjectFlock "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"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -350,7 +353,18 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, id)
|
||||
updated, err := s.GetOne(c, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if updated.UsageQty > 0 {
|
||||
if err := s.syncChickinTraceForProductWarehouse(c.Context(), nil, updated.ProductWarehouseId); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to sync chickin stock trace")
|
||||
}
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
@@ -368,15 +382,31 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if chickin.UsageQty > 0 || chickin.PendingUsageQty > 0 {
|
||||
if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil {
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
chickinRepoTx := repository.NewChickinRepository(tx)
|
||||
|
||||
if chickin.UsageQty > 0 || chickin.PendingUsageQty > 0 {
|
||||
if err := s.ReleaseChickinStocks(c.Context(), tx, chickin, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := chickinRepoTx.DeleteOne(c.Context(), id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
||||
if err := s.syncChickinTraceForProductWarehouse(c.Context(), tx, chickin.ProductWarehouseId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||
return fiberErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -439,6 +469,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
||||
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
|
||||
touchedProductWarehouseIDs := make(map[uint]struct{})
|
||||
|
||||
for _, approvableID := range approvableIDs {
|
||||
if _, err := approvalSvc.CreateApproval(
|
||||
@@ -492,6 +523,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
||||
}
|
||||
chickin.UsageQty = approvedQty
|
||||
chickin.PendingUsageQty = 0
|
||||
touchedProductWarehouseIDs[chickin.ProductWarehouseId] = struct{}{}
|
||||
|
||||
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(c.Context(), chickin.Id)
|
||||
if err != nil {
|
||||
@@ -555,6 +587,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
||||
if err := s.ReleaseChickinStocks(c.Context(), dbTransaction, &chickin, actorID); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for rejected chickin %d: %v", chickin.Id, err))
|
||||
}
|
||||
touchedProductWarehouseIDs[chickin.ProductWarehouseId] = struct{}{}
|
||||
|
||||
if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -564,6 +597,13 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for productWarehouseID := range touchedProductWarehouseIDs {
|
||||
if err := s.syncChickinTraceForProductWarehouse(c.Context(), dbTransaction, productWarehouseID); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to sync chickin trace for product warehouse %d", productWarehouseID))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -678,6 +718,180 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *chickinService) syncChickinTraceForProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) error {
|
||||
if productWarehouseID == 0 {
|
||||
return nil
|
||||
}
|
||||
if s.FifoStockV2Svc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if tx == nil {
|
||||
return s.Repository.DB().WithContext(ctx).Transaction(func(innerTx *gorm.DB) error {
|
||||
return s.syncChickinTraceForProductWarehouse(ctx, innerTx, productWarehouseID)
|
||||
})
|
||||
}
|
||||
|
||||
flagGroupCode, err := s.resolveFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(flagGroupCode) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if err := tx.WithContext(ctx).
|
||||
Table("stock_allocations").
|
||||
Where("product_warehouse_id = ?", productWarehouseID).
|
||||
Where("usable_type = ?", fifo.UsableKeyProjectChickin.String()).
|
||||
Where("allocation_purpose = ?", entity.StockAllocationPurposeTraceChickin).
|
||||
Where("status = ?", entity.StockAllocationStatusActive).
|
||||
Updates(map[string]any{
|
||||
"status": entity.StockAllocationStatusReleased,
|
||||
"released_at": now,
|
||||
"updated_at": now,
|
||||
"note": "chickin_trace_reflow_reset",
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type chickinTraceRow struct {
|
||||
ID uint `gorm:"column:id"`
|
||||
UsageQty float64 `gorm:"column:usage_qty"`
|
||||
ChickIn time.Time `gorm:"column:chick_in_date"`
|
||||
}
|
||||
chickins := make([]chickinTraceRow, 0)
|
||||
if err := tx.WithContext(ctx).
|
||||
Table("project_chickins").
|
||||
Select("id, usage_qty, chick_in_date").
|
||||
Where("product_warehouse_id = ?", productWarehouseID).
|
||||
Where("deleted_at IS NULL").
|
||||
Where("usage_qty > 0").
|
||||
Order("chick_in_date ASC, id ASC").
|
||||
Scan(&chickins).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(chickins) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
gatherRows, err := s.FifoStockV2Svc.Gather(ctx, commonSvc.FifoStockV2GatherRequest{
|
||||
FlagGroupCode: flagGroupCode,
|
||||
Lane: "STOCKABLE",
|
||||
AllocationPurpose: entity.StockAllocationPurposeTraceChickin,
|
||||
IgnoreSourceUsed: true,
|
||||
ProductWarehouseID: productWarehouseID,
|
||||
Limit: 50000,
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(gatherRows) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type lotKey struct {
|
||||
StockableType string
|
||||
StockableID uint
|
||||
}
|
||||
remainingByLot := make(map[lotKey]float64, len(gatherRows))
|
||||
for _, row := range gatherRows {
|
||||
key := lotKey{StockableType: row.Ref.LegacyTypeKey, StockableID: row.Ref.ID}
|
||||
remainingByLot[key] = row.AvailableQuantity
|
||||
}
|
||||
|
||||
lotIndex := 0
|
||||
traceNow := time.Now()
|
||||
for _, chickin := range chickins {
|
||||
remaining := chickin.UsageQty
|
||||
for remaining > 1e-6 && lotIndex < len(gatherRows) {
|
||||
lot := gatherRows[lotIndex]
|
||||
key := lotKey{StockableType: lot.Ref.LegacyTypeKey, StockableID: lot.Ref.ID}
|
||||
available := remainingByLot[key]
|
||||
if available <= 1e-6 {
|
||||
lotIndex++
|
||||
continue
|
||||
}
|
||||
|
||||
portion := math.Min(remaining, available)
|
||||
if portion <= 1e-6 {
|
||||
lotIndex++
|
||||
continue
|
||||
}
|
||||
|
||||
insert := map[string]any{
|
||||
"product_warehouse_id": productWarehouseID,
|
||||
"stockable_type": lot.Ref.LegacyTypeKey,
|
||||
"stockable_id": lot.Ref.ID,
|
||||
"usable_type": fifo.UsableKeyProjectChickin.String(),
|
||||
"usable_id": chickin.ID,
|
||||
"qty": portion,
|
||||
"status": entity.StockAllocationStatusActive,
|
||||
"allocation_purpose": entity.StockAllocationPurposeTraceChickin,
|
||||
"engine_version": "v2",
|
||||
"flag_group_code": flagGroupCode,
|
||||
"function_code": "CHICKIN_TRACE",
|
||||
"created_at": traceNow,
|
||||
"updated_at": traceNow,
|
||||
}
|
||||
if err := tx.WithContext(ctx).Table("stock_allocations").Create(insert).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
remaining -= portion
|
||||
remainingByLot[key] = available - portion
|
||||
}
|
||||
|
||||
if remaining > 1e-6 {
|
||||
s.Log.Warnf(
|
||||
"chickin trace partial allocation for product_warehouse_id=%d chickin_id=%d: remaining=%.3f",
|
||||
productWarehouseID,
|
||||
chickin.ID,
|
||||
remaining,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *chickinService) resolveFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) {
|
||||
type row struct {
|
||||
FlagGroupCode string `gorm:"column:flag_group_code"`
|
||||
}
|
||||
|
||||
selected := row{}
|
||||
err := tx.WithContext(ctx).
|
||||
Table("fifo_stock_v2_route_rules rr").
|
||||
Select("rr.flag_group_code").
|
||||
Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
|
||||
Where("rr.is_active = TRUE").
|
||||
Where("rr.lane = 'STOCKABLE'").
|
||||
Where(`
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM product_warehouses pw
|
||||
JOIN flags f ON f.flagable_id = pw.product_id
|
||||
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
|
||||
WHERE pw.id = ?
|
||||
AND f.flagable_type = ?
|
||||
AND fm.flag_group_code = rr.flag_group_code
|
||||
)
|
||||
`, productWarehouseID, entity.FlagableTypeProduct).
|
||||
Order("fg.priority ASC, rr.id ASC").
|
||||
Limit(1).
|
||||
Take(&selected).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return selected.FlagGroupCode, nil
|
||||
}
|
||||
|
||||
func (s chickinService) EnsureChickInExists(ctx context.Context, projectFlockKandangID uint) error {
|
||||
if projectFlockKandangID == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
|
||||
|
||||
Reference in New Issue
Block a user