mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
fix: first push need support testing, and implemented fifo v2 to all modules
This commit is contained in:
@@ -134,12 +134,7 @@ func main() {
|
|||||||
reflowReq := commonSvc.FifoStockV2ReflowRequest{
|
reflowReq := commonSvc.FifoStockV2ReflowRequest{
|
||||||
FlagGroupCode: route.FlagGroupCode,
|
FlagGroupCode: route.FlagGroupCode,
|
||||||
ProductWarehouseID: adj.ProductWarehouseID,
|
ProductWarehouseID: adj.ProductWarehouseID,
|
||||||
Usable: commonSvc.FifoStockV2Ref{
|
AsOf: &adj.CreatedAt,
|
||||||
ID: adj.ID,
|
|
||||||
LegacyTypeKey: fifo.UsableKeyAdjustmentOut.String(),
|
|
||||||
FunctionCode: route.FunctionCode,
|
|
||||||
},
|
|
||||||
DesiredQty: 0,
|
|
||||||
IdempotencyKey: fmt.Sprintf("delete-adjustment-usable-%d-%d", adj.ID, time.Now().UnixNano()),
|
IdempotencyKey: fmt.Sprintf("delete-adjustment-usable-%d-%d", adj.ID, time.Now().UnixNano()),
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,12 +121,7 @@ func main() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
usableType := fifo.UsableKeyAdjustmentOut.String()
|
activeAllocationCount, err := countActiveAllocations(ctx, db, fifo.UsableKeyAdjustmentOut.String(), adj.ID)
|
||||||
if route.SourceTable == "adjustment_stocks" && strings.TrimSpace(route.LegacyTypeKey) != "" {
|
|
||||||
usableType = strings.TrimSpace(route.LegacyTypeKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
activeAllocationCount, err := countActiveAllocations(ctx, db, usableType, adj.ID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("FAIL adj=%d error=count allocations: %v\n", adj.ID, err)
|
fmt.Printf("FAIL adj=%d error=count allocations: %v\n", adj.ID, err)
|
||||||
failed++
|
failed++
|
||||||
@@ -142,12 +137,6 @@ func main() {
|
|||||||
reflowReq := commonSvc.FifoStockV2ReflowRequest{
|
reflowReq := commonSvc.FifoStockV2ReflowRequest{
|
||||||
FlagGroupCode: route.FlagGroupCode,
|
FlagGroupCode: route.FlagGroupCode,
|
||||||
ProductWarehouseID: adj.ProductWarehouseID,
|
ProductWarehouseID: adj.ProductWarehouseID,
|
||||||
Usable: commonSvc.FifoStockV2Ref{
|
|
||||||
ID: adj.ID,
|
|
||||||
LegacyTypeKey: usableType,
|
|
||||||
FunctionCode: route.FunctionCode,
|
|
||||||
},
|
|
||||||
DesiredQty: desiredQty,
|
|
||||||
IdempotencyKey: fmt.Sprintf("manual-adjustment-reflow-%d-%d", adj.ID, time.Now().UnixNano()),
|
IdempotencyKey: fmt.Sprintf("manual-adjustment-reflow-%d-%d", adj.ID, time.Now().UnixNano()),
|
||||||
}
|
}
|
||||||
if asOfCreatedAt {
|
if asOfCreatedAt {
|
||||||
|
|||||||
@@ -401,12 +401,9 @@ func (s *fifoStockV2Service) rollbackInternal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*ReflowResult, error) {
|
func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*ReflowResult, error) {
|
||||||
if strings.TrimSpace(req.FlagGroupCode) == "" || req.ProductWarehouseID == 0 || req.Usable.ID == 0 || strings.TrimSpace(req.Usable.LegacyTypeKey) == "" {
|
if strings.TrimSpace(req.FlagGroupCode) == "" || req.ProductWarehouseID == 0 {
|
||||||
return nil, fmt.Errorf("%w: invalid reflow request", ErrInvalidRequest)
|
return nil, fmt.Errorf("%w: invalid reflow request", ErrInvalidRequest)
|
||||||
}
|
}
|
||||||
if req.DesiredQty < 0 {
|
|
||||||
return nil, fmt.Errorf("%w: desired qty must be >= 0", ErrInvalidRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := &ReflowResult{}
|
result := &ReflowResult{}
|
||||||
err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
|
||||||
@@ -420,11 +417,7 @@ func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*Re
|
|||||||
hash := requestHash(map[string]any{
|
hash := requestHash(map[string]any{
|
||||||
"flag_group_code": req.FlagGroupCode,
|
"flag_group_code": req.FlagGroupCode,
|
||||||
"product_warehouse_id": req.ProductWarehouseID,
|
"product_warehouse_id": req.ProductWarehouseID,
|
||||||
"usable_type": req.Usable.LegacyTypeKey,
|
|
||||||
"usable_id": req.Usable.ID,
|
|
||||||
"desired_qty": req.DesiredQty,
|
|
||||||
"as_of": req.AsOf,
|
"as_of": req.AsOf,
|
||||||
"allow_over_consume": req.AllowOverConsume,
|
|
||||||
})
|
})
|
||||||
logRow, reused, err := s.beginOperation(
|
logRow, reused, err := s.beginOperation(
|
||||||
tx,
|
tx,
|
||||||
@@ -433,8 +426,8 @@ func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*Re
|
|||||||
hash,
|
hash,
|
||||||
req.ProductWarehouseID,
|
req.ProductWarehouseID,
|
||||||
req.FlagGroupCode,
|
req.FlagGroupCode,
|
||||||
req.Usable.LegacyTypeKey,
|
"",
|
||||||
req.Usable.ID,
|
0,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -456,9 +449,24 @@ func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*Re
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usableRows, gatherErr := s.gatherAllRows(ctx, tx, GatherRequest{
|
||||||
|
FlagGroupCode: req.FlagGroupCode,
|
||||||
|
Lane: LaneUsable,
|
||||||
|
ProductWarehouseID: req.ProductWarehouseID,
|
||||||
|
Limit: s.defaultGatherLimit,
|
||||||
|
})
|
||||||
|
if gatherErr != nil {
|
||||||
|
err = gatherErr
|
||||||
|
return gatherErr
|
||||||
|
}
|
||||||
|
result.ProcessedUsables = len(usableRows)
|
||||||
|
|
||||||
|
for _, usableRow := range usableRows {
|
||||||
|
desiredQty := usableRow.Quantity + usableRow.PendingQuantity
|
||||||
|
|
||||||
rollbackRes, rollbackErr := s.rollbackInternal(ctx, tx, RollbackRequest{
|
rollbackRes, rollbackErr := s.rollbackInternal(ctx, tx, RollbackRequest{
|
||||||
ProductWarehouseID: req.ProductWarehouseID,
|
ProductWarehouseID: req.ProductWarehouseID,
|
||||||
Usable: req.Usable,
|
Usable: usableRow.Ref,
|
||||||
ReleaseQty: nil,
|
ReleaseQty: nil,
|
||||||
Reason: "reflow reset",
|
Reason: "reflow reset",
|
||||||
}, req.FlagGroupCode)
|
}, req.FlagGroupCode)
|
||||||
@@ -466,22 +474,57 @@ func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*Re
|
|||||||
err = rollbackErr
|
err = rollbackErr
|
||||||
return rollbackErr
|
return rollbackErr
|
||||||
}
|
}
|
||||||
result.Rollback = *rollbackRes
|
result.Rollback.ReleasedQty += rollbackRes.ReleasedQty
|
||||||
|
if len(rollbackRes.Details) > 0 {
|
||||||
|
result.Rollback.Details = append(result.Rollback.Details, rollbackRes.Details...)
|
||||||
|
}
|
||||||
|
minDesired := rollbackRes.ReleasedQty + usableRow.PendingQuantity
|
||||||
|
if desiredQty < minDesired {
|
||||||
|
desiredQty = minDesired
|
||||||
|
}
|
||||||
|
|
||||||
if req.DesiredQty > 0 {
|
if desiredQty <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
asOf := usableRow.SortAt
|
||||||
|
if req.AsOf != nil && asOf.Before(*req.AsOf) {
|
||||||
|
asOf = *req.AsOf
|
||||||
|
}
|
||||||
allocateRes, allocateErr := s.allocateInternal(ctx, tx, AllocateRequest{
|
allocateRes, allocateErr := s.allocateInternal(ctx, tx, AllocateRequest{
|
||||||
FlagGroupCode: req.FlagGroupCode,
|
FlagGroupCode: req.FlagGroupCode,
|
||||||
ProductWarehouseID: req.ProductWarehouseID,
|
ProductWarehouseID: req.ProductWarehouseID,
|
||||||
Usable: req.Usable,
|
Usable: usableRow.Ref,
|
||||||
NeedQty: req.DesiredQty,
|
NeedQty: desiredQty,
|
||||||
AllowOverConsume: req.AllowOverConsume,
|
AsOf: &asOf,
|
||||||
AsOf: req.AsOf,
|
|
||||||
})
|
})
|
||||||
if allocateErr != nil {
|
if allocateErr != nil {
|
||||||
err = allocateErr
|
err = allocateErr
|
||||||
return allocateErr
|
return allocateErr
|
||||||
}
|
}
|
||||||
result.Allocate = *allocateRes
|
result.Allocate.AllocatedQty += allocateRes.AllocatedQty
|
||||||
|
result.Allocate.PendingQty += allocateRes.PendingQty
|
||||||
|
if len(allocateRes.Details) > 0 {
|
||||||
|
result.Allocate.Details = append(result.Allocate.Details, allocateRes.Details...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedQty, calcErr := s.calculateWarehouseAvailableForGroup(ctx, tx, req.ProductWarehouseID, req.FlagGroupCode, nil)
|
||||||
|
if calcErr != nil {
|
||||||
|
err = calcErr
|
||||||
|
return calcErr
|
||||||
|
}
|
||||||
|
actualQty, loadErr := s.loadWarehouseQty(ctx, tx, req.ProductWarehouseID)
|
||||||
|
if loadErr != nil {
|
||||||
|
err = loadErr
|
||||||
|
return loadErr
|
||||||
|
}
|
||||||
|
drift := expectedQty - actualQty
|
||||||
|
if math.Abs(drift) >= 1e-6 {
|
||||||
|
if adjustErr := s.adjustProductWarehouseQty(tx, req.ProductWarehouseID, drift); adjustErr != nil {
|
||||||
|
err = adjustErr
|
||||||
|
return adjustErr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if finishErr := s.finishOperation(tx, logRow, result); finishErr != nil {
|
if finishErr := s.finishOperation(tx, logRow, result); finishErr != nil {
|
||||||
@@ -496,6 +539,54 @@ func (s *fifoStockV2Service) Reflow(ctx context.Context, req ReflowRequest) (*Re
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *fifoStockV2Service) gatherAllRows(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
req GatherRequest,
|
||||||
|
) ([]GatherRow, error) {
|
||||||
|
limit := req.Limit
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = s.defaultGatherLimit
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Limit = limit
|
||||||
|
out := make([]GatherRow, 0, limit)
|
||||||
|
|
||||||
|
var cursorSortAt *time.Time
|
||||||
|
cursorSourceTable := ""
|
||||||
|
var cursorSourceID uint
|
||||||
|
|
||||||
|
for {
|
||||||
|
req.AfterSortAt = cursorSortAt
|
||||||
|
req.AfterSourceTable = cursorSourceTable
|
||||||
|
req.AfterSourceID = cursorSourceID
|
||||||
|
|
||||||
|
rows, err := s.gatherRows(ctx, tx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, rows...)
|
||||||
|
if len(rows) < limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
last := rows[len(rows)-1]
|
||||||
|
lastSortAt := last.SortAt
|
||||||
|
cursorSortAt = &lastSortAt
|
||||||
|
cursorSourceTable = last.SourceTable
|
||||||
|
cursorSourceID = last.SourceID
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *fifoStockV2Service) loadActiveAllocations(
|
func (s *fifoStockV2Service) loadActiveAllocations(
|
||||||
tx *gorm.DB,
|
tx *gorm.DB,
|
||||||
usableType string,
|
usableType string,
|
||||||
|
|||||||
@@ -197,6 +197,9 @@ func (s *fifoStockV2Service) buildGatherSubquery(rule routeRule, trait traitRule
|
|||||||
if req.AsOf != nil {
|
if req.AsOf != nil {
|
||||||
whereParts = append(whereParts, fmt.Sprintf("%s <= ?", sortExpr))
|
whereParts = append(whereParts, fmt.Sprintf("%s <= ?", sortExpr))
|
||||||
}
|
}
|
||||||
|
if req.From != nil {
|
||||||
|
whereParts = append(whereParts, fmt.Sprintf("%s >= ?", sortExpr))
|
||||||
|
}
|
||||||
|
|
||||||
if rule.ScopeSQL != nil && strings.TrimSpace(*rule.ScopeSQL) != "" {
|
if rule.ScopeSQL != nil && strings.TrimSpace(*rule.ScopeSQL) != "" {
|
||||||
whereParts = append(whereParts, fmt.Sprintf("(%s)", normalizeScopeSQL(*rule.ScopeSQL)))
|
whereParts = append(whereParts, fmt.Sprintf("(%s)", normalizeScopeSQL(*rule.ScopeSQL)))
|
||||||
@@ -236,6 +239,9 @@ func (s *fifoStockV2Service) buildGatherSubquery(rule routeRule, trait traitRule
|
|||||||
if req.AsOf != nil {
|
if req.AsOf != nil {
|
||||||
args = append(args, *req.AsOf)
|
args = append(args, *req.AsOf)
|
||||||
}
|
}
|
||||||
|
if req.From != nil {
|
||||||
|
args = append(args, *req.From)
|
||||||
|
}
|
||||||
|
|
||||||
return subquery, args, nil
|
return subquery, args, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ type GatherRequest struct {
|
|||||||
FlagGroupCode string
|
FlagGroupCode string
|
||||||
Lane Lane
|
Lane Lane
|
||||||
ProductWarehouseID uint
|
ProductWarehouseID uint
|
||||||
|
From *time.Time
|
||||||
AsOf *time.Time
|
AsOf *time.Time
|
||||||
Limit int
|
Limit int
|
||||||
AfterSortAt *time.Time
|
AfterSortAt *time.Time
|
||||||
@@ -98,15 +99,13 @@ type RollbackResult struct {
|
|||||||
type ReflowRequest struct {
|
type ReflowRequest struct {
|
||||||
FlagGroupCode string
|
FlagGroupCode string
|
||||||
ProductWarehouseID uint
|
ProductWarehouseID uint
|
||||||
Usable Ref
|
|
||||||
DesiredQty float64
|
|
||||||
AllowOverConsume *bool
|
|
||||||
IdempotencyKey string
|
|
||||||
AsOf *time.Time
|
AsOf *time.Time
|
||||||
|
IdempotencyKey string
|
||||||
Tx *gorm.DB
|
Tx *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReflowResult struct {
|
type ReflowResult struct {
|
||||||
|
ProcessedUsables int
|
||||||
Rollback RollbackResult
|
Rollback RollbackResult
|
||||||
Allocate AllocateResult
|
Allocate AllocateResult
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/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"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -167,15 +166,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
|
|
||||||
transactionType := utils.ResolveAdjustmentTransactionType(routeMeta.FunctionCode)
|
transactionType := utils.ResolveAdjustmentTransactionType(routeMeta.FunctionCode)
|
||||||
|
|
||||||
allowPending := false
|
|
||||||
if routeMeta.Lane == adjustmentLaneUsable {
|
|
||||||
allowPending, err = s.resolveOverconsumePolicy(ctx, routeMeta)
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to resolve overconsume rule: %+v", err)
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to resolve FIFO policy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var createdAdjustmentStockId uint
|
var createdAdjustmentStockId uint
|
||||||
|
|
||||||
var projectFlockKandangID *uint
|
var projectFlockKandangID *uint
|
||||||
@@ -228,6 +218,12 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
Price: req.Price,
|
Price: req.Price,
|
||||||
GrandTotal: grandTotal,
|
GrandTotal: grandTotal,
|
||||||
}
|
}
|
||||||
|
switch routeMeta.Lane {
|
||||||
|
case adjustmentLaneStockable:
|
||||||
|
adjustmentStock.TotalQty = qty
|
||||||
|
case adjustmentLaneUsable:
|
||||||
|
adjustmentStock.UsageQty = qty
|
||||||
|
}
|
||||||
code, err := adjustmentStockRepoTX.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix)
|
code, err := adjustmentStockRepoTX.GenerateSequentialNumber(ctx, utils.AdjustmentStockNumberPrefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -240,60 +236,32 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
var increaseQty float64
|
var increaseQty float64
|
||||||
var decreaseQty float64
|
var decreaseQty float64
|
||||||
|
|
||||||
switch routeMeta.Lane {
|
if routeMeta.Lane != adjustmentLaneStockable && routeMeta.Lane != adjustmentLaneUsable {
|
||||||
case adjustmentLaneStockable:
|
return fiber.NewError(fiber.StatusBadRequest, "Unsupported transaction subtype lane")
|
||||||
fifoNote := fmt.Sprintf("Stock Adjustment %s #%s", routeMeta.FunctionCode, adjustmentStock.AdjNumber)
|
|
||||||
result, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
|
|
||||||
StockableKey: fifo.StockableKeyAdjustmentIn,
|
|
||||||
StockableID: adjustmentStock.Id,
|
|
||||||
ProductWarehouseID: productWarehouse.Id,
|
|
||||||
Quantity: qty,
|
|
||||||
Note: &fifoNote,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock via FIFO: %v", err))
|
|
||||||
}
|
}
|
||||||
increaseQty = result.AddedQuantity
|
if s.FifoStockV2Svc == nil {
|
||||||
case adjustmentLaneUsable:
|
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
|
||||||
if s.FifoStockV2Svc != nil {
|
|
||||||
usableLegacyTypeKey := fifo.UsableKeyAdjustmentOut.String()
|
|
||||||
if routeMeta.SourceTable == "adjustment_stocks" && strings.TrimSpace(routeMeta.LegacyTypeKey) != "" {
|
|
||||||
usableLegacyTypeKey = strings.TrimSpace(routeMeta.LegacyTypeKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reflowResult, err := s.FifoStockV2Svc.Reflow(ctx, common.FifoStockV2ReflowRequest{
|
asOf := adjustmentStock.CreatedAt
|
||||||
|
if _, err := s.FifoStockV2Svc.Reflow(ctx, common.FifoStockV2ReflowRequest{
|
||||||
FlagGroupCode: routeMeta.FlagGroupCode,
|
FlagGroupCode: routeMeta.FlagGroupCode,
|
||||||
ProductWarehouseID: productWarehouse.Id,
|
ProductWarehouseID: productWarehouse.Id,
|
||||||
Usable: common.FifoStockV2Ref{
|
AsOf: &asOf,
|
||||||
ID: adjustmentStock.Id,
|
|
||||||
LegacyTypeKey: usableLegacyTypeKey,
|
|
||||||
FunctionCode: routeMeta.FunctionCode,
|
|
||||||
},
|
|
||||||
DesiredQty: qty,
|
|
||||||
AllowOverConsume: &allowPending,
|
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
})
|
}); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshedAdjustment, err := adjustmentStockRepoTX.GetByID(ctx, adjustmentStock.Id, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to consume stock via FIFO v2: %v", err))
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh adjustment stock")
|
||||||
}
|
}
|
||||||
decreaseQty = reflowResult.Allocate.AllocatedQty
|
switch routeMeta.Lane {
|
||||||
} else {
|
case adjustmentLaneStockable:
|
||||||
result, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
|
increaseQty = refreshedAdjustment.TotalQty
|
||||||
UsableKey: fifo.UsableKeyAdjustmentOut,
|
case adjustmentLaneUsable:
|
||||||
UsableID: adjustmentStock.Id,
|
decreaseQty = refreshedAdjustment.UsageQty
|
||||||
ProductWarehouseID: productWarehouse.Id,
|
|
||||||
Quantity: qty,
|
|
||||||
AllowPending: allowPending,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to consume stock via FIFO: %v", err))
|
|
||||||
}
|
|
||||||
decreaseQty = result.UsageQuantity
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Unsupported transaction subtype lane")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stockLogs, err := stockLogRepoTX.GetByProductWarehouse(ctx, productWarehouse.Id, 1)
|
stockLogs, err := stockLogRepoTX.GetByProductWarehouse(ctx, productWarehouse.Id, 1)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/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"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -444,83 +443,79 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pakanProducts := map[uint]bool{}
|
if s.FifoStockV2Svc == nil {
|
||||||
if s.FifoStockV2Svc != nil && len(req.Products) > 0 {
|
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
|
||||||
pakanProducts, err = s.resolvePakanProducts(c.Context(), tx, req.Products)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
flagGroupByProduct := make(map[uint]string, len(req.Products))
|
||||||
|
|
||||||
for _, product := range req.Products {
|
for _, product := range req.Products {
|
||||||
detail := detailMap[uint64(product.ProductID)]
|
detail := detailMap[uint64(product.ProductID)]
|
||||||
|
if detail == nil || detail.SourceProductWarehouseID == nil || detail.DestProductWarehouseID == nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Data transfer detail tidak valid")
|
||||||
|
}
|
||||||
|
|
||||||
outUsageQty := 0.0
|
flagGroupCode, ok := flagGroupByProduct[uint(product.ProductID)]
|
||||||
outPendingQty := 0.0
|
if !ok {
|
||||||
useFifoV2 := s.FifoStockV2Svc != nil && pakanProducts[uint(product.ProductID)]
|
flagGroupCode, err = s.resolveTransferFlagGroup(c.Context(), tx, uint(product.ProductID))
|
||||||
if useFifoV2 {
|
|
||||||
s.Log.Infof(
|
|
||||||
"[fifo-v2][transfer] use reflow movement=%s detail_id=%d product_id=%d source_pw=%d qty=%.3f",
|
|
||||||
entityTransfer.MovementNumber,
|
|
||||||
detail.Id,
|
|
||||||
product.ProductID,
|
|
||||||
*detail.SourceProductWarehouseID,
|
|
||||||
product.ProductQty,
|
|
||||||
)
|
|
||||||
reflowResult, err := s.FifoStockV2Svc.Reflow(c.Context(), commonSvc.FifoStockV2ReflowRequest{
|
|
||||||
FlagGroupCode: "PAKAN",
|
|
||||||
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
|
|
||||||
Usable: commonSvc.FifoStockV2Ref{
|
|
||||||
ID: uint(detail.Id),
|
|
||||||
LegacyTypeKey: fifo.UsableKeyStockTransferOut.String(),
|
|
||||||
FunctionCode: "STOCK_TRANSFER_OUT",
|
|
||||||
},
|
|
||||||
DesiredQty: product.ProductQty,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("FIFO v2 route tidak ditemukan untuk produk %d: %v", product.ProductID, err))
|
||||||
}
|
}
|
||||||
outUsageQty = reflowResult.Allocate.AllocatedQty
|
flagGroupByProduct[uint(product.ProductID)] = flagGroupCode
|
||||||
outPendingQty = reflowResult.Allocate.PendingQty
|
|
||||||
s.Log.Infof(
|
|
||||||
"[fifo-v2][transfer] reflow result movement=%s detail_id=%d usage=%.3f pending=%.3f",
|
|
||||||
entityTransfer.MovementNumber,
|
|
||||||
detail.Id,
|
|
||||||
outUsageQty,
|
|
||||||
outPendingQty,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
|
|
||||||
UsableKey: fifo.UsableKeyStockTransferOut,
|
|
||||||
UsableID: uint(detail.Id),
|
|
||||||
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
|
|
||||||
Quantity: product.ProductQty,
|
|
||||||
AllowPending: false,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
|
|
||||||
}
|
|
||||||
outUsageQty = consumeResult.UsageQuantity
|
|
||||||
outPendingQty = consumeResult.PendingQuantity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||||
Where("id = ?", detail.Id).
|
Where("id = ?", detail.Id).
|
||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
"usage_qty": outUsageQty,
|
"usage_qty": product.ProductQty,
|
||||||
"pending_qty": outPendingQty,
|
"pending_qty": 0,
|
||||||
|
"total_qty": product.ProductQty,
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
s.Log.Errorf("Failed to update tracking usage for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
|
s.Log.Errorf("Failed to update transfer detail seed fields for detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memperbarui data tracking")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
asOf := transferDate
|
||||||
|
if _, err := s.FifoStockV2Svc.Reflow(c.Context(), commonSvc.FifoStockV2ReflowRequest{
|
||||||
|
FlagGroupCode: flagGroupCode,
|
||||||
|
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
|
||||||
|
AsOf: &asOf,
|
||||||
|
Tx: tx,
|
||||||
|
}); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal. Error: %v", product.ProductID, err))
|
||||||
|
}
|
||||||
|
if _, err := s.FifoStockV2Svc.Reflow(c.Context(), commonSvc.FifoStockV2ReflowRequest{
|
||||||
|
FlagGroupCode: flagGroupCode,
|
||||||
|
ProductWarehouseID: uint(*detail.DestProductWarehouseID),
|
||||||
|
AsOf: &asOf,
|
||||||
|
Tx: tx,
|
||||||
|
}); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal reflow stok tujuan untuk produk %d. Error: %v", product.ProductID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
type usageSnapshot struct {
|
||||||
|
UsageQty float64 `gorm:"column:usage_qty"`
|
||||||
|
PendingQty float64 `gorm:"column:pending_qty"`
|
||||||
|
}
|
||||||
|
var usage usageSnapshot
|
||||||
|
if err := tx.WithContext(c.Context()).
|
||||||
|
Table("stock_transfer_details").
|
||||||
|
Select("usage_qty, pending_qty").
|
||||||
|
Where("id = ?", detail.Id).
|
||||||
|
Take(&usage).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to read transfer usage snapshot detail_id=%d, product_id=%d: %+v", detail.Id, product.ProductID, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data tracking")
|
||||||
|
}
|
||||||
|
outUsageQty := usage.UsageQty
|
||||||
|
outPendingQty := usage.PendingQty
|
||||||
|
if outPendingQty > 1e-6 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk produk %d di gudang asal", product.ProductID))
|
||||||
|
}
|
||||||
|
|
||||||
stockLogDecrease := &entity.StockLog{
|
stockLogDecrease := &entity.StockLog{
|
||||||
ProductWarehouseId: uint(*detail.SourceProductWarehouseID),
|
ProductWarehouseId: uint(*detail.SourceProductWarehouseID),
|
||||||
CreatedBy: uint(actorID),
|
CreatedBy: uint(actorID),
|
||||||
Increase: 0,
|
Increase: 0,
|
||||||
Decrease: product.ProductQty,
|
Decrease: outUsageQty,
|
||||||
LoggableType: string(utils.StockLogTypeTransfer),
|
LoggableType: string(utils.StockLogTypeTransfer),
|
||||||
LoggableId: uint(detail.Id),
|
LoggableId: uint(detail.Id),
|
||||||
Notes: "",
|
Notes: "",
|
||||||
@@ -541,45 +536,12 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
|
||||||
}
|
}
|
||||||
|
|
||||||
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
inAddedQty := outUsageQty
|
||||||
inAddedQty := 0.0
|
|
||||||
if useFifoV2 {
|
|
||||||
s.Log.Infof(
|
|
||||||
"[fifo-v2][transfer] stock-in uses replenish path movement=%s detail_id=%d product_id=%d dest_pw=%d qty=%.3f",
|
|
||||||
entityTransfer.MovementNumber,
|
|
||||||
detail.Id,
|
|
||||||
product.ProductID,
|
|
||||||
*detail.DestProductWarehouseID,
|
|
||||||
product.ProductQty,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
|
||||||
StockableKey: fifo.StockableKeyStockTransferIn,
|
|
||||||
StockableID: uint(detail.Id),
|
|
||||||
ProductWarehouseID: uint(*detail.DestProductWarehouseID),
|
|
||||||
Quantity: product.ProductQty,
|
|
||||||
Note: ¬e,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
inAddedQty = replenishResult.AddedQuantity
|
|
||||||
|
|
||||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
|
||||||
Where("id = ?", detail.Id).
|
|
||||||
Updates(map[string]interface{}{
|
|
||||||
"total_qty": inAddedQty,
|
|
||||||
}).Error; err != nil {
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
stockLogIncrease := &entity.StockLog{
|
stockLogIncrease := &entity.StockLog{
|
||||||
ProductWarehouseId: uint(*detail.DestProductWarehouseID),
|
ProductWarehouseId: uint(*detail.DestProductWarehouseID),
|
||||||
CreatedBy: uint(actorID),
|
CreatedBy: uint(actorID),
|
||||||
Increase: product.ProductQty,
|
Increase: inAddedQty,
|
||||||
Decrease: 0,
|
Decrease: 0,
|
||||||
LoggableType: string(utils.StockLogTypeTransfer),
|
LoggableType: string(utils.StockLogTypeTransfer),
|
||||||
LoggableId: uint(detail.Id),
|
LoggableId: uint(detail.Id),
|
||||||
@@ -657,51 +619,45 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *transferService) resolvePakanProducts(
|
func (s *transferService) resolveTransferFlagGroup(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tx *gorm.DB,
|
tx *gorm.DB,
|
||||||
products []validation.TransferProduct,
|
productID uint,
|
||||||
) (map[uint]bool, error) {
|
) (string, error) {
|
||||||
out := make(map[uint]bool, len(products))
|
if productID == 0 {
|
||||||
if len(products) == 0 {
|
return "", fmt.Errorf("product id is required")
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
productIDs := make([]uint, 0, len(products))
|
|
||||||
seen := make(map[uint]struct{}, len(products))
|
|
||||||
for _, product := range products {
|
|
||||||
if product.ProductID == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[product.ProductID]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[product.ProductID] = struct{}{}
|
|
||||||
productIDs = append(productIDs, product.ProductID)
|
|
||||||
}
|
|
||||||
if len(productIDs) == 0 {
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type row struct {
|
type row struct {
|
||||||
ProductID uint `gorm:"column:product_id"`
|
FlagGroupCode string `gorm:"column:flag_group_code"`
|
||||||
}
|
}
|
||||||
var rows []row
|
var selected row
|
||||||
err := tx.WithContext(ctx).
|
err := tx.WithContext(ctx).
|
||||||
Table("flags f").
|
Table("fifo_stock_v2_route_rules rr").
|
||||||
Select("DISTINCT f.flagable_id AS product_id").
|
Select("rr.flag_group_code").
|
||||||
Where("f.flagable_type = ?", entity.FlagableTypeProduct).
|
Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
|
||||||
Where("f.name IN ?", []string{"PAKAN", "PRE-STARTER", "STARTER", "FINISHER"}).
|
Where("rr.is_active = TRUE").
|
||||||
Where("f.flagable_id IN ?", productIDs).
|
Where("rr.lane = ?", "USABLE").
|
||||||
Scan(&rows).Error
|
Where("rr.function_code = ?", "STOCK_TRANSFER_OUT").
|
||||||
|
Where("rr.source_table = ?", "stock_transfer_details").
|
||||||
|
Where(`
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM flags f
|
||||||
|
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
|
||||||
|
WHERE f.flagable_type = ?
|
||||||
|
AND f.flagable_id = ?
|
||||||
|
AND fm.flag_group_code = rr.flag_group_code
|
||||||
|
)
|
||||||
|
`, entity.FlagableTypeProduct, productID).
|
||||||
|
Order("rr.id ASC").
|
||||||
|
Limit(1).
|
||||||
|
Take(&selected).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, row := range rows {
|
return strings.TrimSpace(selected.FlagGroupCode), nil
|
||||||
out[row.ProductID] = true
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID uint64, payloads []TransferExpenseReceivingPayload) error {
|
func (s *transferService) notifyExpenseItemsDelivered(c *fiber.Ctx, transferID uint64, payloads []TransferExpenseReceivingPayload) error {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package marketing
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -20,7 +19,6 @@ import (
|
|||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type MarketingModule struct{}
|
type MarketingModule struct{}
|
||||||
@@ -35,24 +33,7 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
stockLogRepo := rShared.NewStockLogRepository(db)
|
stockLogRepo := rShared.NewStockLogRepository(db)
|
||||||
|
|
||||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
|
||||||
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
|
||||||
|
|
||||||
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
|
||||||
Key: fifo.UsableKeyMarketingDelivery,
|
|
||||||
Table: "marketing_delivery_products",
|
|
||||||
Columns: fifo.UsableColumns{
|
|
||||||
ID: "id",
|
|
||||||
ProductWarehouseID: "product_warehouse_id",
|
|
||||||
UsageQuantity: "usage_qty",
|
|
||||||
PendingQuantity: "pending_qty",
|
|
||||||
CreatedAt: "created_at",
|
|
||||||
},
|
|
||||||
}); err != nil {
|
|
||||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
|
||||||
panic(fmt.Sprintf("failed to register marketing delivery usable workflow: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||||
@@ -64,8 +45,8 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
|
|
||||||
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, fifoService, warehouseRepo, projectFlockKandangRepo, validate)
|
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, fifoStockV2Service, warehouseRepo, projectFlockKandangRepo, validate)
|
||||||
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoService, validate)
|
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoStockV2Service, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
RegisterRoutes(router, userService, salesOrdersService, deliveryOrdersService)
|
RegisterRoutes(router, userService, salesOrdersService, deliveryOrdersService)
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||||
rShared "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rShared "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"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -36,7 +35,7 @@ type deliveryOrdersService struct {
|
|||||||
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
|
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
|
||||||
StockLogRepo rShared.StockLogRepository
|
StockLogRepo rShared.StockLogRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
FifoSvc commonSvc.FifoService
|
FifoStockV2Svc commonSvc.FifoStockV2Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDeliveryOrdersService(
|
func NewDeliveryOrdersService(
|
||||||
@@ -45,7 +44,7 @@ func NewDeliveryOrdersService(
|
|||||||
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
|
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
|
||||||
stockLogRepo rShared.StockLogRepository,
|
stockLogRepo rShared.StockLogRepository,
|
||||||
approvalSvc commonSvc.ApprovalService,
|
approvalSvc commonSvc.ApprovalService,
|
||||||
fifoSvc commonSvc.FifoService,
|
fifoStockV2Svc commonSvc.FifoStockV2Service,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
) DeliveryOrdersService {
|
) DeliveryOrdersService {
|
||||||
return &deliveryOrdersService{
|
return &deliveryOrdersService{
|
||||||
@@ -55,7 +54,7 @@ func NewDeliveryOrdersService(
|
|||||||
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||||
StockLogRepo: stockLogRepo,
|
StockLogRepo: stockLogRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
FifoSvc: fifoSvc,
|
FifoStockV2Svc: fifoStockV2Svc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -549,33 +548,42 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Delivery product not found")
|
return fiber.NewError(fiber.StatusInternalServerError, "Delivery product not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
|
||||||
UsableKey: fifo.UsableKeyMarketingDelivery,
|
|
||||||
UsableID: deliveryProduct.Id,
|
|
||||||
ProductWarehouseID: marketingProduct.ProductWarehouseId,
|
|
||||||
Quantity: requestedQty,
|
|
||||||
AllowPending: false,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
|
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
|
||||||
|
previousUsage := deliveryProduct.UsageQty
|
||||||
|
deliveryProduct.UsageQty = requestedQty
|
||||||
|
deliveryProduct.PendingQty = 0
|
||||||
|
|
||||||
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, 0); err != nil {
|
if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
|
||||||
}
|
}
|
||||||
|
|
||||||
if actorID > 0 && result.UsageQuantity > 0 {
|
if err := reflowMarketingScope(
|
||||||
|
ctx,
|
||||||
|
s.FifoStockV2Svc,
|
||||||
|
tx,
|
||||||
|
marketingProduct.ProductWarehouseId,
|
||||||
|
resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
|
||||||
|
); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshed, err := deliveryProductRepo.GetByID(ctx, deliveryProduct.Id, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh delivery product")
|
||||||
|
}
|
||||||
|
deliveryProduct.UsageQty = refreshed.UsageQty
|
||||||
|
deliveryProduct.PendingQty = refreshed.PendingQty
|
||||||
|
deliveryProduct.CreatedAt = refreshed.CreatedAt
|
||||||
|
|
||||||
|
allocatedDelta := deliveryProduct.UsageQty - previousUsage
|
||||||
|
if actorID > 0 && allocatedDelta > 0 {
|
||||||
decreaseLog := &entity.StockLog{
|
decreaseLog := &entity.StockLog{
|
||||||
Decrease: result.UsageQuantity,
|
Decrease: allocatedDelta,
|
||||||
LoggableType: string(utils.StockLogTypeMarketing),
|
LoggableType: string(utils.StockLogTypeMarketing),
|
||||||
LoggableId: deliveryProduct.Id,
|
LoggableId: deliveryProduct.Id,
|
||||||
ProductWarehouseId: marketingProduct.ProductWarehouseId,
|
ProductWarehouseId: marketingProduct.ProductWarehouseId,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
Notes: fmt.Sprintf("FIFO consume (%.2f)", result.UsageQuantity),
|
Notes: fmt.Sprintf("FIFO v2 reflow consume (%.2f)", allocatedDelta),
|
||||||
}
|
}
|
||||||
|
|
||||||
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
|
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
|
||||||
@@ -604,35 +612,45 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
|
|||||||
}
|
}
|
||||||
|
|
||||||
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
|
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
|
||||||
currentUsage, err := deliveryProductRepo.GetUsageQty(ctx, deliveryProduct.Id)
|
currentUsage := deliveryProduct.UsageQty
|
||||||
if err != nil {
|
currentPending := deliveryProduct.PendingQty
|
||||||
currentUsage = 0
|
if currentUsage <= 0 && currentPending <= 0 {
|
||||||
}
|
|
||||||
|
|
||||||
if currentUsage == 0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
|
deliveryProduct.UsageQty = 0
|
||||||
UsableKey: fifo.UsableKeyMarketingDelivery,
|
deliveryProduct.PendingQty = 0
|
||||||
UsableID: deliveryProduct.Id,
|
if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil {
|
||||||
Tx: tx,
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset delivery product")
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil {
|
if err := reflowMarketingScope(
|
||||||
return err
|
ctx,
|
||||||
|
s.FifoStockV2Svc,
|
||||||
|
tx,
|
||||||
|
marketingProduct.ProductWarehouseId,
|
||||||
|
resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
|
||||||
|
); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if actorID > 0 && currentUsage > 0 {
|
refreshed, err := deliveryProductRepo.GetByID(ctx, deliveryProduct.Id, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh delivery product")
|
||||||
|
}
|
||||||
|
deliveryProduct.UsageQty = refreshed.UsageQty
|
||||||
|
deliveryProduct.PendingQty = refreshed.PendingQty
|
||||||
|
deliveryProduct.CreatedAt = refreshed.CreatedAt
|
||||||
|
|
||||||
|
releasedUsage := currentUsage - deliveryProduct.UsageQty
|
||||||
|
if actorID > 0 && releasedUsage > 0 {
|
||||||
increaseLog := &entity.StockLog{
|
increaseLog := &entity.StockLog{
|
||||||
Increase: currentUsage,
|
Increase: releasedUsage,
|
||||||
LoggableType: string(utils.StockLogTypeMarketing),
|
LoggableType: string(utils.StockLogTypeMarketing),
|
||||||
LoggableId: deliveryProduct.Id,
|
LoggableId: deliveryProduct.Id,
|
||||||
ProductWarehouseId: marketingProduct.ProductWarehouseId,
|
ProductWarehouseId: marketingProduct.ProductWarehouseId,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
Notes: fmt.Sprintf("Release delivery stock (%.2f)", currentUsage),
|
Notes: fmt.Sprintf("FIFO v2 reflow release (%.2f)", releasedUsage),
|
||||||
}
|
}
|
||||||
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
|
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
marketingOutFunctionCode = "MARKETING_OUT"
|
||||||
|
marketingUsableLane = "USABLE"
|
||||||
|
marketingSourceTable = "marketing_delivery_products"
|
||||||
|
)
|
||||||
|
|
||||||
|
func reflowMarketingScope(
|
||||||
|
ctx context.Context,
|
||||||
|
fifoStockV2Svc commonSvc.FifoStockV2Service,
|
||||||
|
tx *gorm.DB,
|
||||||
|
productWarehouseID uint,
|
||||||
|
asOf *time.Time,
|
||||||
|
) error {
|
||||||
|
if fifoStockV2Svc == nil {
|
||||||
|
return fmt.Errorf("FIFO v2 service is not available")
|
||||||
|
}
|
||||||
|
if productWarehouseID == 0 {
|
||||||
|
return fmt.Errorf("product warehouse id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
flagGroupCode, err := resolveMarketingFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(flagGroupCode) == "" {
|
||||||
|
return fmt.Errorf("flag group code is not found for product warehouse %d", productWarehouseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fifoStockV2Svc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
|
||||||
|
FlagGroupCode: flagGroupCode,
|
||||||
|
ProductWarehouseID: productWarehouseID,
|
||||||
|
AsOf: asOf,
|
||||||
|
Tx: tx,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveMarketingFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) {
|
||||||
|
type row struct {
|
||||||
|
FlagGroupCode string `gorm:"column:flag_group_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var 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 = ?", marketingUsableLane).
|
||||||
|
Where("rr.function_code = ?", marketingOutFunctionCode).
|
||||||
|
Where("rr.source_table = ?", marketingSourceTable).
|
||||||
|
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("rr.id ASC").
|
||||||
|
Limit(1).
|
||||||
|
Take(&selected).Error
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(selected.FlagGroupCode), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveMarketingAsOf(deliveryDate, createdAt *time.Time) *time.Time {
|
||||||
|
if deliveryDate != nil {
|
||||||
|
asOf := *deliveryDate
|
||||||
|
return &asOf
|
||||||
|
}
|
||||||
|
if createdAt != nil {
|
||||||
|
asOf := *createdAt
|
||||||
|
return &asOf
|
||||||
|
}
|
||||||
|
asOf := time.Now()
|
||||||
|
return &asOf
|
||||||
|
}
|
||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -43,12 +42,12 @@ type salesOrdersService struct {
|
|||||||
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
|
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
|
||||||
UserRepo userRepo.UserRepository
|
UserRepo userRepo.UserRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
FifoSvc commonSvc.FifoService
|
FifoStockV2Svc commonSvc.FifoStockV2Service
|
||||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, fifoSvc commonSvc.FifoService, warehouseRepo warehouseRepo.WarehouseRepository,
|
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, fifoStockV2Svc commonSvc.FifoStockV2Service, warehouseRepo warehouseRepo.WarehouseRepository,
|
||||||
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService {
|
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService {
|
||||||
return &salesOrdersService{
|
return &salesOrdersService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
@@ -58,7 +57,7 @@ func NewSalesOrdersService(marketingRepo repository.MarketingRepository, custome
|
|||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
UserRepo: userRepo,
|
UserRepo: userRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
FifoSvc: fifoSvc,
|
FifoStockV2Svc: fifoStockV2Svc,
|
||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
}
|
}
|
||||||
@@ -376,15 +375,18 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
|||||||
if qtyDiff < 0 {
|
if qtyDiff < 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Cannot decrease quantity after stock has been allocated. Please delete and create new product.")
|
return fiber.NewError(fiber.StatusBadRequest, "Cannot decrease quantity after stock has been allocated. Please delete and create new product.")
|
||||||
} else if qtyDiff > 0 {
|
} else if qtyDiff > 0 {
|
||||||
_, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
|
nextRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty + qtyDiff
|
||||||
UsableKey: fifo.UsableKeyMarketingDelivery,
|
if err := invDeliveryRepoTx.UpdateFifoFields(c.Context(), deliveryProduct.Id, nextRequestedQty, 0); err != nil {
|
||||||
UsableID: deliveryProduct.Id,
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing delivery fifo fields")
|
||||||
ProductWarehouseID: rp.ProductWarehouseId,
|
}
|
||||||
Quantity: qtyDiff,
|
if err := reflowMarketingScope(
|
||||||
Tx: dbTransaction,
|
c.Context(),
|
||||||
})
|
s.FifoStockV2Svc,
|
||||||
if err != nil {
|
dbTransaction,
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Insufficient stock for additional quantity: %v", err))
|
rp.ProductWarehouseId,
|
||||||
|
resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
|
||||||
|
); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,12 +441,17 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
|||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has been delivered", old.Id))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has been delivered", old.Id))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{
|
if err := invDeliveryRepoTx.UpdateFifoFields(c.Context(), deliveryProduct.Id, 0, 0); err != nil {
|
||||||
UsableKey: fifo.UsableKeyMarketingDelivery,
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset marketing delivery fifo fields")
|
||||||
UsableID: deliveryProduct.Id,
|
}
|
||||||
Tx: dbTransaction,
|
if err := reflowMarketingScope(
|
||||||
}); err != nil {
|
c.Context(),
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock: %v", err))
|
s.FifoStockV2Svc,
|
||||||
|
dbTransaction,
|
||||||
|
deliveryProduct.ProductWarehouseId,
|
||||||
|
resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
|
||||||
|
); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil {
|
if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil {
|
||||||
@@ -523,12 +530,17 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
deliveryProducts, err := marketingDeliveryProductRepoTx.GetByMarketingId(c.Context(), marketing.Id)
|
deliveryProducts, err := marketingDeliveryProductRepoTx.GetByMarketingId(c.Context(), marketing.Id)
|
||||||
if err == nil && len(deliveryProducts) > 0 {
|
if err == nil && len(deliveryProducts) > 0 {
|
||||||
for _, dp := range deliveryProducts {
|
for _, dp := range deliveryProducts {
|
||||||
if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{
|
if err := marketingDeliveryProductRepoTx.UpdateFifoFields(c.Context(), dp.Id, 0, 0); err != nil {
|
||||||
UsableKey: fifo.UsableKeyMarketingDelivery,
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reset fifo fields for delivery product %d", dp.Id))
|
||||||
UsableID: dp.Id,
|
}
|
||||||
Tx: dbTransaction,
|
if err := reflowMarketingScope(
|
||||||
}); err != nil {
|
c.Context(),
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for delivery product %d: %v", dp.Id, err))
|
s.FifoStockV2Svc,
|
||||||
|
dbTransaction,
|
||||||
|
dp.ProductWarehouseId,
|
||||||
|
resolveMarketingAsOf(dp.DeliveryDate, dp.CreatedAt),
|
||||||
|
); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock via FIFO v2 for delivery product %d: %v", dp.Id, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package chickins
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -10,7 +9,6 @@ import (
|
|||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
|
||||||
|
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
@@ -40,45 +38,9 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
productRepo := rProduct.NewProductRepository(db)
|
productRepo := rProduct.NewProductRepository(db)
|
||||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
|
||||||
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
|
||||||
Key: fifo.UsableKeyProjectChickin,
|
|
||||||
Table: "project_chickins",
|
|
||||||
Columns: fifo.UsableColumns{
|
|
||||||
ID: "id",
|
|
||||||
ProductWarehouseID: "product_warehouse_id",
|
|
||||||
UsageQuantity: "usage_qty",
|
|
||||||
PendingQuantity: "pending_usage_qty",
|
|
||||||
CreatedAt: "created_at",
|
|
||||||
},
|
|
||||||
|
|
||||||
ExcludedStockables: []fifo.StockableKey{fifo.StockableKeyProjectFlockPopulation},
|
|
||||||
}); err != nil {
|
|
||||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
|
||||||
panic(fmt.Sprintf("failed to register chickin usable workflow: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil {
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil {
|
||||||
@@ -96,7 +58,7 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
projectflockpopulationrepo,
|
projectflockpopulationrepo,
|
||||||
chickinDetailRepo,
|
chickinDetailRepo,
|
||||||
validate,
|
validate,
|
||||||
fifoService)
|
fifoStockV2Service)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ChickinRoutes(router, userService, chickinService)
|
ChickinRoutes(router, userService, chickinService)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/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"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -27,8 +26,6 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var chickinUsableKey = fifo.UsableKeyProjectChickin
|
|
||||||
|
|
||||||
type ChickinService interface {
|
type ChickinService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
|
||||||
@@ -51,11 +48,11 @@ type chickinService struct {
|
|||||||
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
|
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
|
||||||
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
|
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
|
||||||
ProjectChickinDetailRepo repository.ProjectChickinDetailRepository
|
ProjectChickinDetailRepo repository.ProjectChickinDetailRepository
|
||||||
FifoSvc commonSvc.FifoService
|
FifoStockV2Svc commonSvc.FifoStockV2Service
|
||||||
StockLogRepo rStockLogs.StockLogRepository
|
StockLogRepo rStockLogs.StockLogRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, productRepo rProduct.ProductRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoSvc commonSvc.FifoService) ChickinService {
|
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, productRepo rProduct.ProductRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoStockV2Svc commonSvc.FifoStockV2Service) ChickinService {
|
||||||
return &chickinService{
|
return &chickinService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
@@ -68,7 +65,7 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan
|
|||||||
ProjectflockKandangRepo: projectflockkandangRepo,
|
ProjectflockKandangRepo: projectflockkandangRepo,
|
||||||
ProjectflockPopulationRepo: projectflockpopulationRepo,
|
ProjectflockPopulationRepo: projectflockpopulationRepo,
|
||||||
ProjectChickinDetailRepo: projectChickinDetailRepo,
|
ProjectChickinDetailRepo: projectChickinDetailRepo,
|
||||||
FifoSvc: fifoSvc,
|
FifoStockV2Svc: fifoStockV2Svc,
|
||||||
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
|
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,18 +369,9 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if chickin.UsageQty > 0 {
|
if chickin.UsageQty > 0 {
|
||||||
|
|
||||||
currentUsageQty := chickin.UsageQty
|
|
||||||
|
|
||||||
if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil {
|
if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
warehouseDeltas := make(map[uint]float64)
|
|
||||||
warehouseDeltas[chickin.ProductWarehouseId] += currentUsageQty
|
|
||||||
if err := s.adjustProductWarehouseQuantities(c.Context(), s.Repository.DB(), warehouseDeltas); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||||
@@ -549,12 +537,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for rejected chickin %d: %v", chickin.Id, err))
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for rejected chickin %d: %v", chickin.Id, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
warehouseDeltas := make(map[uint]float64)
|
|
||||||
warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty
|
|
||||||
if err := s.adjustProductWarehouseQuantities(c.Context(), dbTransaction, warehouseDeltas); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil {
|
if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil {
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to delete rejected chickin %d", chickin.Id))
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to delete rejected chickin %d", chickin.Id))
|
||||||
@@ -617,36 +599,48 @@ func (s *chickinService) autoAddFlagToProduct(ctx context.Context, tx *gorm.DB,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, desiredQty float64, actorID uint) error {
|
func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, desiredQty float64, actorID uint) error {
|
||||||
if chickin == nil || s.FifoSvc == nil {
|
if chickin == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if tx == nil {
|
||||||
|
return errors.New("transaction is required")
|
||||||
|
}
|
||||||
|
if s.FifoStockV2Svc == nil {
|
||||||
|
return errors.New("fifo v2 service is not available")
|
||||||
|
}
|
||||||
|
if desiredQty < 0 {
|
||||||
|
return errors.New("desired quantity must be zero or greater")
|
||||||
|
}
|
||||||
|
|
||||||
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, desiredQty, 0); err != nil {
|
||||||
UsableKey: chickinUsableKey,
|
|
||||||
UsableID: chickin.Id,
|
|
||||||
ProductWarehouseID: chickin.ProductWarehouseId,
|
|
||||||
Quantity: desiredQty,
|
|
||||||
AllowPending: true,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
asOf := chickin.ChickInDate
|
||||||
|
if asOf.IsZero() {
|
||||||
|
asOf = chickin.CreatedAt
|
||||||
|
}
|
||||||
|
if err := reflowChickinScope(ctx, s.FifoStockV2Svc, tx, chickin.ProductWarehouseId, &asOf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.UsageQuantity > 0 {
|
var refreshed entity.ProjectChickin
|
||||||
|
if err := tx.WithContext(ctx).
|
||||||
|
Where("id = ?", chickin.Id).
|
||||||
|
Take(&refreshed).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if refreshed.UsageQty > 0 {
|
||||||
decreaseLog := &entity.StockLog{
|
decreaseLog := &entity.StockLog{
|
||||||
Decrease: result.UsageQuantity,
|
Decrease: refreshed.UsageQty,
|
||||||
LoggableType: string(utils.StockLogTypeChikin),
|
LoggableType: string(utils.StockLogTypeChikin),
|
||||||
LoggableId: chickin.Id,
|
LoggableId: refreshed.Id,
|
||||||
ProductWarehouseId: chickin.ProductWarehouseId,
|
ProductWarehouseId: refreshed.ProductWarehouseId,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
Notes: fmt.Sprintf("Chickin #%d", chickin.Id),
|
Notes: fmt.Sprintf("Chickin #%d", refreshed.Id),
|
||||||
}
|
}
|
||||||
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, chickin.ProductWarehouseId, 1)
|
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, refreshed.ProductWarehouseId, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
||||||
}
|
}
|
||||||
@@ -658,46 +652,52 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
decreaseLog.Stock -= decreaseLog.Decrease
|
decreaseLog.Stock -= decreaseLog.Decrease
|
||||||
}
|
}
|
||||||
|
|
||||||
s.StockLogRepo.CreateOne(ctx, decreaseLog, nil)
|
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, decreaseLog, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, targetPW *entity.ProductWarehouse, population *entity.ProjectFlockPopulation, actorID uint) error {
|
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 {
|
if chickin == nil || targetPW == nil || population == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if tx == nil {
|
||||||
|
return errors.New("transaction is required")
|
||||||
|
}
|
||||||
|
if s.FifoStockV2Svc == nil {
|
||||||
|
return errors.New("fifo v2 service is not available")
|
||||||
|
}
|
||||||
|
|
||||||
_, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
if err := tx.WithContext(ctx).
|
||||||
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
Model(&entity.ProjectFlockPopulation{}).
|
||||||
StockableID: population.Id,
|
Where("id = ?", population.Id).
|
||||||
ProductWarehouseID: targetPW.Id,
|
Update("total_qty", chickin.UsageQty).Error; err != nil {
|
||||||
Quantity: chickin.UsageQty,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
asOf := chickin.ChickInDate
|
||||||
|
if asOf.IsZero() {
|
||||||
|
asOf = chickin.CreatedAt
|
||||||
|
}
|
||||||
|
return reflowChickinScope(ctx, s.FifoStockV2Svc, tx, targetPW.Id, &asOf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, actorID uint) error {
|
func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, actorID uint) error {
|
||||||
if chickin == nil || s.FifoSvc == nil {
|
if chickin == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if tx == nil {
|
||||||
|
return errors.New("transaction is required")
|
||||||
|
}
|
||||||
|
if s.FifoStockV2Svc == nil {
|
||||||
|
return errors.New("fifo v2 service is not available")
|
||||||
|
}
|
||||||
|
|
||||||
var currentUsage float64
|
var currentUsage float64
|
||||||
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(¤tUsage).Error; err != nil {
|
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(¤tUsage).Error; err != nil {
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
|
|
||||||
UsableKey: chickinUsableKey,
|
|
||||||
UsableID: chickin.Id,
|
|
||||||
Tx: tx,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,6 +705,14 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
asOf := chickin.ChickInDate
|
||||||
|
if asOf.IsZero() {
|
||||||
|
asOf = chickin.CreatedAt
|
||||||
|
}
|
||||||
|
if err := reflowChickinScope(ctx, s.FifoStockV2Svc, tx, chickin.ProductWarehouseId, &asOf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if currentUsage > 0 {
|
if currentUsage > 0 {
|
||||||
increaseLog := &entity.StockLog{
|
increaseLog := &entity.StockLog{
|
||||||
Increase: currentUsage,
|
Increase: currentUsage,
|
||||||
@@ -726,7 +734,9 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
increaseLog.Stock += increaseLog.Increase
|
increaseLog.Stock += increaseLog.Increase
|
||||||
}
|
}
|
||||||
|
|
||||||
s.StockLogRepo.CreateOne(ctx, increaseLog, nil)
|
if err := s.StockLogRepo.WithTx(tx).CreateOne(ctx, increaseLog, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -755,10 +765,3 @@ func (s chickinService) EnsureChickInExists(ctx context.Context, projectFlockKan
|
|||||||
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording")
|
return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *chickinService) adjustProductWarehouseQuantities(ctx context.Context, tx *gorm.DB, deltas map[uint]float64) error {
|
|
||||||
if len(deltas) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return s.ProductWarehouseRepo.AdjustQuantities(ctx, deltas, func(*gorm.DB) *gorm.DB { return tx })
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
chickinOutFunctionCode = "CHICKIN_OUT"
|
||||||
|
chickinUsableLane = "USABLE"
|
||||||
|
chickinSourceTable = "project_chickins"
|
||||||
|
)
|
||||||
|
|
||||||
|
func reflowChickinScope(
|
||||||
|
ctx context.Context,
|
||||||
|
fifoStockV2Svc commonSvc.FifoStockV2Service,
|
||||||
|
tx *gorm.DB,
|
||||||
|
productWarehouseID uint,
|
||||||
|
asOf *time.Time,
|
||||||
|
) error {
|
||||||
|
if fifoStockV2Svc == nil {
|
||||||
|
return fmt.Errorf("FIFO v2 service is not available")
|
||||||
|
}
|
||||||
|
if tx == nil {
|
||||||
|
return fmt.Errorf("transaction is required")
|
||||||
|
}
|
||||||
|
if productWarehouseID == 0 {
|
||||||
|
return fmt.Errorf("product warehouse id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
flagGroupCode, err := resolveChickinFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(flagGroupCode) == "" {
|
||||||
|
return fmt.Errorf("flag group code is not found for product warehouse %d", productWarehouseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fifoStockV2Svc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
|
||||||
|
FlagGroupCode: flagGroupCode,
|
||||||
|
ProductWarehouseID: productWarehouseID,
|
||||||
|
AsOf: asOf,
|
||||||
|
Tx: tx,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveChickinFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) {
|
||||||
|
type row struct {
|
||||||
|
FlagGroupCode string `gorm:"column:flag_group_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var 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 = ?", chickinUsableLane).
|
||||||
|
Where("rr.function_code = ?", chickinOutFunctionCode).
|
||||||
|
Where("rr.source_table = ?", chickinSourceTable).
|
||||||
|
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("rr.id ASC").
|
||||||
|
Limit(1).
|
||||||
|
Take(&selected).Error
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(selected.FlagGroupCode), nil
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package recordings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -26,7 +25,6 @@ import (
|
|||||||
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/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"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
|
||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -48,7 +46,6 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
productRepo := rProduct.NewProductRepository(db)
|
productRepo := rProduct.NewProductRepository(db)
|
||||||
chickinRepo := rChickin.NewChickinRepository(db)
|
chickinRepo := rChickin.NewChickinRepository(db)
|
||||||
chickinDetailRepo := rChickin.NewChickinDetailRepository(db)
|
chickinDetailRepo := rChickin.NewChickinDetailRepository(db)
|
||||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
|
||||||
stockLogRepo := rStockLogs.NewStockLogRepository(db)
|
stockLogRepo := rStockLogs.NewStockLogRepository(db)
|
||||||
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
|
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
|
||||||
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
|
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
|
||||||
@@ -61,76 +58,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
validate,
|
validate,
|
||||||
)
|
)
|
||||||
|
|
||||||
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
|
||||||
if err := fifoService.RegisterStockable(fifo.StockableConfig{
|
|
||||||
Key: fifo.StockableKeyRecordingEgg,
|
|
||||||
Table: "recording_eggs",
|
|
||||||
Columns: fifo.StockableColumns{
|
|
||||||
ID: "id",
|
|
||||||
ProductWarehouseID: "product_warehouse_id",
|
|
||||||
TotalQuantity: "total_qty",
|
|
||||||
TotalUsedQuantity: "total_used",
|
|
||||||
CreatedAt: "(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_eggs.recording_id)",
|
|
||||||
},
|
|
||||||
OrderBy: []string{"(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_eggs.recording_id) ASC", "id ASC"},
|
|
||||||
}); err != nil {
|
|
||||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
|
||||||
panic(fmt.Sprintf("failed to register recording egg stockable workflow: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := fifoService.RegisterStockable(fifo.StockableConfig{
|
|
||||||
Key: fifo.StockableKeyRecordingDepletion,
|
|
||||||
Table: "recording_depletions",
|
|
||||||
Columns: fifo.StockableColumns{
|
|
||||||
ID: "id",
|
|
||||||
ProductWarehouseID: "product_warehouse_id",
|
|
||||||
TotalQuantity: "qty",
|
|
||||||
TotalUsedQuantity: "total_used_qty",
|
|
||||||
CreatedAt: "(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_depletions.recording_id)",
|
|
||||||
},
|
|
||||||
OrderBy: []string{"(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_depletions.recording_id) ASC", "id ASC"},
|
|
||||||
}); err != nil {
|
|
||||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
|
||||||
panic(fmt.Sprintf("failed to register recording depletion stockable workflow: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
|
||||||
Key: fifo.UsableKeyRecordingStock,
|
|
||||||
Table: "recording_stocks",
|
|
||||||
Columns: fifo.UsableColumns{
|
|
||||||
ID: "id",
|
|
||||||
ProductWarehouseID: "product_warehouse_id",
|
|
||||||
UsageQuantity: "usage_qty",
|
|
||||||
PendingQuantity: "pending_qty",
|
|
||||||
CreatedAt: "(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_stocks.recording_id)",
|
|
||||||
},
|
|
||||||
}); err != nil {
|
|
||||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
|
||||||
panic(fmt.Sprintf("failed to register recording usable workflow: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
|
||||||
Key: fifo.UsableKeyRecordingDepletion,
|
|
||||||
Table: "recording_depletions",
|
|
||||||
Columns: fifo.UsableColumns{
|
|
||||||
ID: "id",
|
|
||||||
ProductWarehouseID: "source_product_warehouse_id",
|
|
||||||
UsageQuantity: "usage_qty",
|
|
||||||
PendingQuantity: "pending_qty",
|
|
||||||
CreatedAt: "(SELECT r.record_datetime FROM recordings r WHERE r.id = recording_depletions.recording_id)",
|
|
||||||
},
|
|
||||||
ExcludedStockables: []fifo.StockableKey{
|
|
||||||
fifo.StockableKeyTransferToLayingIn,
|
|
||||||
fifo.StockableKeyStockTransferIn,
|
|
||||||
fifo.StockableKeyAdjustmentIn,
|
|
||||||
fifo.StockableKeyPurchaseItems,
|
|
||||||
fifo.StockableKeyRecordingEgg,
|
|
||||||
},
|
|
||||||
}); err != nil {
|
|
||||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
|
||||||
panic(fmt.Sprintf("failed to register recording depletion usable workflow: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
@@ -169,7 +97,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
projectFlockPopulationRepo,
|
projectFlockPopulationRepo,
|
||||||
chickinDetailRepo,
|
chickinDetailRepo,
|
||||||
validate,
|
validate,
|
||||||
fifoService,
|
fifoStockV2Service,
|
||||||
)
|
)
|
||||||
|
|
||||||
recordingService := sRecording.NewRecordingService(
|
recordingService := sRecording.NewRecordingService(
|
||||||
@@ -179,7 +107,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
projectFlockPopulationRepo,
|
projectFlockPopulationRepo,
|
||||||
approvalRepo,
|
approvalRepo,
|
||||||
approvalService,
|
approvalService,
|
||||||
fifoService,
|
fifoStockV2Service,
|
||||||
stockLogRepo,
|
stockLogRepo,
|
||||||
productionStandardService,
|
productionStandardService,
|
||||||
projectFlockService,
|
projectFlockService,
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
recordingLaneUsable = "USABLE"
|
||||||
|
recordingLaneStockable = "STOCKABLE"
|
||||||
|
|
||||||
|
recordingFunctionStockOut = "RECORDING_STOCK_OUT"
|
||||||
|
recordingFunctionDepletionOut = "RECORDING_DEPLETION_OUT"
|
||||||
|
recordingFunctionDepletionIn = "RECORDING_DEPLETION_IN"
|
||||||
|
recordingFunctionEggIn = "RECORDING_EGG_IN"
|
||||||
|
|
||||||
|
recordingSourceStocks = "recording_stocks"
|
||||||
|
recordingSourceDepletions = "recording_depletions"
|
||||||
|
recordingSourceEggs = "recording_eggs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *recordingService) reflowRecordingScope(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
productWarehouseID uint,
|
||||||
|
recordingID uint,
|
||||||
|
lane string,
|
||||||
|
functionCode string,
|
||||||
|
sourceTable string,
|
||||||
|
) error {
|
||||||
|
if s == nil || s.FifoStockV2Svc == nil {
|
||||||
|
return fmt.Errorf("FIFO v2 service is not available")
|
||||||
|
}
|
||||||
|
if tx == nil {
|
||||||
|
return fmt.Errorf("transaction is required")
|
||||||
|
}
|
||||||
|
if productWarehouseID == 0 {
|
||||||
|
return fmt.Errorf("product warehouse id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
flagGroupCode, err := resolveRecordingFlagGroupByProductWarehouse(ctx, tx, productWarehouseID, lane, functionCode, sourceTable)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(flagGroupCode) == "" {
|
||||||
|
return fmt.Errorf("flag group code is not found for product warehouse %d", productWarehouseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
asOf, err := resolveRecordingAsOf(ctx, tx, recordingID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.FifoStockV2Svc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
|
||||||
|
FlagGroupCode: flagGroupCode,
|
||||||
|
ProductWarehouseID: productWarehouseID,
|
||||||
|
AsOf: asOf,
|
||||||
|
Tx: tx,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveRecordingFlagGroupByProductWarehouse(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
productWarehouseID uint,
|
||||||
|
lane string,
|
||||||
|
functionCode string,
|
||||||
|
sourceTable string,
|
||||||
|
) (string, error) {
|
||||||
|
type row struct {
|
||||||
|
FlagGroupCode string `gorm:"column:flag_group_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var selected row
|
||||||
|
q := 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 = ?", lane).
|
||||||
|
Where("rr.source_table = ?", sourceTable)
|
||||||
|
|
||||||
|
if strings.TrimSpace(functionCode) != "" {
|
||||||
|
q = q.Where("rr.function_code = ?", functionCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := q.
|
||||||
|
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("rr.id ASC").
|
||||||
|
Limit(1).
|
||||||
|
Take(&selected).Error
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(selected.FlagGroupCode), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveRecordingAsOf(ctx context.Context, tx *gorm.DB, recordingID uint) (*time.Time, error) {
|
||||||
|
if recordingID == 0 {
|
||||||
|
asOf := time.Now().UTC()
|
||||||
|
return &asOf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type row struct {
|
||||||
|
RecordDatetime time.Time `gorm:"column:record_datetime"`
|
||||||
|
}
|
||||||
|
var selected row
|
||||||
|
if err := tx.WithContext(ctx).
|
||||||
|
Table("recordings").
|
||||||
|
Select("record_datetime").
|
||||||
|
Where("id = ?", recordingID).
|
||||||
|
Limit(1).
|
||||||
|
Take(&selected).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
asOf := selected.RecordDatetime.UTC()
|
||||||
|
return &asOf, nil
|
||||||
|
}
|
||||||
@@ -52,7 +52,7 @@ type recordingService struct {
|
|||||||
ProductionStandardSvc sProductionStandard.ProductionStandardService
|
ProductionStandardSvc sProductionStandard.ProductionStandardService
|
||||||
ProjectFlockSvc sProjectFlock.ProjectflockService
|
ProjectFlockSvc sProjectFlock.ProjectflockService
|
||||||
ChickinSvc sChickin.ChickinService
|
ChickinSvc sChickin.ChickinService
|
||||||
FifoSvc commonSvc.FifoService
|
FifoStockV2Svc commonSvc.FifoStockV2Service
|
||||||
StockLogRepo rStockLogs.StockLogRepository
|
StockLogRepo rStockLogs.StockLogRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ func NewRecordingService(
|
|||||||
projectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository,
|
projectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository,
|
||||||
approvalRepo commonRepo.ApprovalRepository,
|
approvalRepo commonRepo.ApprovalRepository,
|
||||||
approvalSvc commonSvc.ApprovalService,
|
approvalSvc commonSvc.ApprovalService,
|
||||||
fifoSvc commonSvc.FifoService,
|
fifoStockV2Svc commonSvc.FifoStockV2Service,
|
||||||
stockLogRepo rStockLogs.StockLogRepository,
|
stockLogRepo rStockLogs.StockLogRepository,
|
||||||
productionStandardSvc sProductionStandard.ProductionStandardService,
|
productionStandardSvc sProductionStandard.ProductionStandardService,
|
||||||
projectFlockSvc sProjectFlock.ProjectflockService,
|
projectFlockSvc sProjectFlock.ProjectflockService,
|
||||||
@@ -82,7 +82,7 @@ func NewRecordingService(
|
|||||||
ProductionStandardSvc: productionStandardSvc,
|
ProductionStandardSvc: productionStandardSvc,
|
||||||
ProjectFlockSvc: projectFlockSvc,
|
ProjectFlockSvc: projectFlockSvc,
|
||||||
ChickinSvc: chickinSvc,
|
ChickinSvc: chickinSvc,
|
||||||
FifoSvc: fifoSvc,
|
FifoStockV2Svc: fifoStockV2Svc,
|
||||||
StockLogRepo: stockLogRepo,
|
StockLogRepo: stockLogRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
@@ -18,9 +17,6 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var recordingStockUsableKey = fifo.UsableKeyRecordingStock
|
|
||||||
var recordingDepletionUsableKey = fifo.UsableKeyRecordingDepletion
|
|
||||||
|
|
||||||
const depletionUsageTolerance = 0.000001
|
const depletionUsageTolerance = 0.000001
|
||||||
|
|
||||||
func (s *recordingService) logStockTrace(action string, stock entity.RecordingStock, extra string) {
|
func (s *recordingService) logStockTrace(action string, stock entity.RecordingStock, extra string) {
|
||||||
@@ -101,9 +97,9 @@ func (s *recordingService) consumeRecordingStocks(
|
|||||||
if len(stocks) == 0 {
|
if len(stocks) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if s.FifoSvc == nil {
|
if s.FifoStockV2Svc == nil {
|
||||||
s.Log.Errorf("FIFO service is not available for consuming recording stocks")
|
s.Log.Errorf("FIFO v2 service is not available for consuming recording stocks")
|
||||||
return errors.New("fifo service is not available")
|
return errors.New("fifo v2 service is not available")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
|
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
|
||||||
return errors.New("stock log repository is not available")
|
return errors.New("stock log repository is not available")
|
||||||
@@ -125,38 +121,52 @@ func (s *recordingService) consumeRecordingStocks(
|
|||||||
}
|
}
|
||||||
desiredTotal := desired + pending
|
desiredTotal := desired + pending
|
||||||
|
|
||||||
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
if err := s.Repository.UpdateStockUsage(tx, stock.Id, desiredTotal, 0); err != nil {
|
||||||
UsableKey: recordingStockUsableKey,
|
return err
|
||||||
UsableID: stock.Id,
|
}
|
||||||
ProductWarehouseID: stock.ProductWarehouseId,
|
if err := s.reflowRecordingScope(
|
||||||
Quantity: desiredTotal,
|
ctx,
|
||||||
AllowPending: true,
|
tx,
|
||||||
Tx: tx,
|
stock.ProductWarehouseId,
|
||||||
})
|
stock.RecordingId,
|
||||||
if err != nil {
|
recordingLaneUsable,
|
||||||
s.Log.Errorf("Failed to consume FIFO stock for recording stock %d: %+v", stock.Id, err)
|
recordingFunctionStockOut,
|
||||||
|
recordingSourceStocks,
|
||||||
|
); err != nil {
|
||||||
|
s.Log.Errorf("Failed to reflow FIFO v2 stock for recording stock %d: %+v", stock.Id, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.UpdateStockUsage(tx, stock.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
var refreshed entity.RecordingStock
|
||||||
|
if err := tx.WithContext(ctx).
|
||||||
|
Where("id = ?", stock.Id).
|
||||||
|
Take(&refreshed).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.logStockTrace("consume:done", stock, fmt.Sprintf("desired=%.3f used=%.3f pending=%.3f", desiredTotal, result.UsageQuantity, result.PendingQuantity))
|
actualUsage := 0.0
|
||||||
|
actualPending := 0.0
|
||||||
|
if refreshed.UsageQty != nil {
|
||||||
|
actualUsage = *refreshed.UsageQty
|
||||||
|
}
|
||||||
|
if refreshed.PendingQty != nil {
|
||||||
|
actualPending = *refreshed.PendingQty
|
||||||
|
}
|
||||||
|
s.logStockTrace("consume:done", refreshed, fmt.Sprintf("desired=%.3f used=%.3f pending=%.3f", desiredTotal, actualUsage, actualPending))
|
||||||
|
|
||||||
logDecrease := result.UsageQuantity
|
logDecrease := actualUsage
|
||||||
if result.PendingQuantity > 0 {
|
if actualPending > 0 {
|
||||||
logDecrease += result.PendingQuantity
|
logDecrease += actualPending
|
||||||
}
|
}
|
||||||
if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
||||||
log := &entity.StockLog{
|
log := &entity.StockLog{
|
||||||
ProductWarehouseId: stock.ProductWarehouseId,
|
ProductWarehouseId: refreshed.ProductWarehouseId,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
Decrease: logDecrease,
|
Decrease: logDecrease,
|
||||||
LoggableType: string(utils.StockLogTypeRecording),
|
LoggableType: string(utils.StockLogTypeRecording),
|
||||||
LoggableId: stock.RecordingId,
|
LoggableId: refreshed.RecordingId,
|
||||||
Notes: note,
|
Notes: note,
|
||||||
}
|
}
|
||||||
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, stock.ProductWarehouseId, 1)
|
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, refreshed.ProductWarehouseId, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
||||||
}
|
}
|
||||||
@@ -187,9 +197,9 @@ func (s *recordingService) consumeRecordingDepletions(
|
|||||||
if len(depletions) == 0 {
|
if len(depletions) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if s.FifoSvc == nil {
|
if s.FifoStockV2Svc == nil {
|
||||||
s.Log.Errorf("FIFO service is not available for consuming recording depletions")
|
s.Log.Errorf("FIFO v2 service is not available for consuming recording depletions")
|
||||||
return errors.New("fifo service is not available")
|
return errors.New("fifo v2 service is not available")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
|
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
|
||||||
return errors.New("stock log repository is not available")
|
return errors.New("stock log repository is not available")
|
||||||
@@ -210,27 +220,40 @@ func (s *recordingService) consumeRecordingDepletions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
desired := depletion.Qty + depletion.PendingQty
|
desired := depletion.Qty + depletion.PendingQty
|
||||||
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
if err := tx.WithContext(ctx).
|
||||||
UsableKey: recordingDepletionUsableKey,
|
Model(&entity.RecordingDepletion{}).
|
||||||
UsableID: depletion.Id,
|
Where("id = ?", depletion.Id).
|
||||||
ProductWarehouseID: sourceWarehouseID,
|
Updates(map[string]any{
|
||||||
Quantity: desired,
|
"qty": desired,
|
||||||
AllowPending: false,
|
"usage_qty": desired,
|
||||||
Tx: tx,
|
"pending_qty": 0,
|
||||||
})
|
}).Error; err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
s.Log.Errorf("Failed to consume FIFO stock for recording depletion %d: %+v", depletion.Id, err)
|
}
|
||||||
|
if err := s.reflowRecordingScope(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
sourceWarehouseID,
|
||||||
|
depletion.RecordingId,
|
||||||
|
recordingLaneUsable,
|
||||||
|
recordingFunctionDepletionOut,
|
||||||
|
recordingSourceDepletions,
|
||||||
|
); err != nil {
|
||||||
|
s.Log.Errorf("Failed to reflow FIFO v2 stock for recording depletion %d: %+v", depletion.Id, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, result.PendingQuantity); err != nil {
|
var refreshed entity.RecordingDepletion
|
||||||
|
if err := tx.WithContext(ctx).
|
||||||
|
Where("id = ?", depletion.Id).
|
||||||
|
Take(&refreshed).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.logDepletionTrace("consume:done", depletion, fmt.Sprintf("desired=%.3f used=%.3f pending=%.3f", desired, result.UsageQuantity, result.PendingQuantity))
|
s.logDepletionTrace("consume:done", refreshed, fmt.Sprintf("desired=%.3f used=%.3f pending=%.3f", desired, refreshed.UsageQty, refreshed.PendingQty))
|
||||||
|
|
||||||
logDecrease := result.UsageQuantity
|
logDecrease := refreshed.UsageQty
|
||||||
if result.PendingQuantity > 0 {
|
if refreshed.PendingQty > 0 {
|
||||||
logDecrease += result.PendingQuantity
|
logDecrease += refreshed.PendingQty
|
||||||
}
|
}
|
||||||
if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
if logDecrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
||||||
log := &entity.StockLog{
|
log := &entity.StockLog{
|
||||||
@@ -238,7 +261,7 @@ func (s *recordingService) consumeRecordingDepletions(
|
|||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
Decrease: logDecrease,
|
Decrease: logDecrease,
|
||||||
LoggableType: string(utils.StockLogTypeRecording),
|
LoggableType: string(utils.StockLogTypeRecording),
|
||||||
LoggableId: depletion.RecordingId,
|
LoggableId: refreshed.RecordingId,
|
||||||
Notes: note,
|
Notes: note,
|
||||||
}
|
}
|
||||||
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, sourceWarehouseID, 1)
|
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, sourceWarehouseID, 1)
|
||||||
@@ -258,20 +281,20 @@ func (s *recordingService) consumeRecordingDepletions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
destDelta := depletion.Qty + depletion.PendingQty
|
destDelta := refreshed.Qty + refreshed.PendingQty
|
||||||
if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
if refreshed.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
||||||
if depletion.ProductWarehouseId == sourceWarehouseID {
|
if refreshed.ProductWarehouseId == sourceWarehouseID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log := &entity.StockLog{
|
log := &entity.StockLog{
|
||||||
ProductWarehouseId: depletion.ProductWarehouseId,
|
ProductWarehouseId: refreshed.ProductWarehouseId,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
Increase: destDelta,
|
Increase: destDelta,
|
||||||
LoggableType: string(utils.StockLogTypeRecording),
|
LoggableType: string(utils.StockLogTypeRecording),
|
||||||
LoggableId: depletion.RecordingId,
|
LoggableId: refreshed.RecordingId,
|
||||||
Notes: note,
|
Notes: note,
|
||||||
}
|
}
|
||||||
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, depletion.ProductWarehouseId, 1)
|
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, refreshed.ProductWarehouseId, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
||||||
}
|
}
|
||||||
@@ -302,9 +325,9 @@ func (s *recordingService) releaseRecordingStocks(
|
|||||||
if len(stocks) == 0 {
|
if len(stocks) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if s.FifoSvc == nil {
|
if s.FifoStockV2Svc == nil {
|
||||||
s.Log.Errorf("FIFO service is not available for releasing recording stocks")
|
s.Log.Errorf("FIFO v2 service is not available for releasing recording stocks")
|
||||||
return errors.New("fifo service is not available")
|
return errors.New("fifo v2 service is not available")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
|
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
|
||||||
return errors.New("stock log repository is not available")
|
return errors.New("stock log repository is not available")
|
||||||
@@ -314,45 +337,35 @@ func (s *recordingService) releaseRecordingStocks(
|
|||||||
if stock.Id == 0 {
|
if stock.Id == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if stock.UsageQty != nil && *stock.UsageQty > 0 {
|
|
||||||
activeCount, err := s.countActiveAllocations(ctx, tx, fifo.UsableKeyRecordingStock, stock.Id)
|
currentUsage := 0.0
|
||||||
if err != nil {
|
if stock.UsageQty != nil {
|
||||||
return err
|
currentUsage = *stock.UsageQty
|
||||||
}
|
|
||||||
if activeCount == 0 {
|
|
||||||
s.Log.Warnf("recording-stock release: no active allocations, forcing usage/pending to 0 (stock_id=%d)", stock.Id)
|
|
||||||
if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := s.resyncStockableUsageFromAllocations(ctx, tx, fifo.UsableKeyRecordingStock, stock.Id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := s.ensureActiveAllocations(ctx, tx, fifo.UsableKeyRecordingStock, stock.Id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
s.logStockTrace("release:start", stock, "")
|
s.logStockTrace("release:start", stock, "")
|
||||||
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
|
|
||||||
UsableKey: recordingStockUsableKey,
|
|
||||||
UsableID: stock.Id,
|
|
||||||
Tx: tx,
|
|
||||||
}); err != nil {
|
|
||||||
s.Log.Errorf("Failed to release FIFO stock for recording stock %d: %+v", stock.Id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil {
|
if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := s.reflowRecordingScope(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
stock.ProductWarehouseId,
|
||||||
|
stock.RecordingId,
|
||||||
|
recordingLaneUsable,
|
||||||
|
recordingFunctionStockOut,
|
||||||
|
recordingSourceStocks,
|
||||||
|
); err != nil {
|
||||||
|
s.Log.Errorf("Failed to reflow FIFO v2 release for recording stock %d: %+v", stock.Id, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
s.logStockTrace("release:done", stock, "")
|
s.logStockTrace("release:done", stock, "")
|
||||||
|
|
||||||
if stock.UsageQty != nil && *stock.UsageQty > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
if currentUsage > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
||||||
log := &entity.StockLog{
|
log := &entity.StockLog{
|
||||||
ProductWarehouseId: stock.ProductWarehouseId,
|
ProductWarehouseId: stock.ProductWarehouseId,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
Increase: *stock.UsageQty,
|
Increase: currentUsage,
|
||||||
LoggableType: string(utils.StockLogTypeRecording),
|
LoggableType: string(utils.StockLogTypeRecording),
|
||||||
LoggableId: stock.RecordingId,
|
LoggableId: stock.RecordingId,
|
||||||
Notes: note,
|
Notes: note,
|
||||||
@@ -388,9 +401,9 @@ func (s *recordingService) releaseRecordingDepletions(
|
|||||||
if len(depletions) == 0 {
|
if len(depletions) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if s.FifoSvc == nil {
|
if s.FifoStockV2Svc == nil {
|
||||||
s.Log.Errorf("FIFO service is not available for releasing recording depletions")
|
s.Log.Errorf("FIFO v2 service is not available for releasing recording depletions")
|
||||||
return errors.New("fifo service is not available")
|
return errors.New("fifo v2 service is not available")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
|
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
|
||||||
return errors.New("stock log repository is not available")
|
return errors.New("stock log repository is not available")
|
||||||
@@ -400,36 +413,7 @@ func (s *recordingService) releaseRecordingDepletions(
|
|||||||
if depletion.Id == 0 {
|
if depletion.Id == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if depletion.UsageQty > 0 {
|
|
||||||
activeCount, err := s.countActiveAllocations(ctx, tx, fifo.UsableKeyRecordingDepletion, depletion.Id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if activeCount == 0 {
|
|
||||||
s.Log.Warnf("recording-depletion release: no active allocations, forcing usage/pending to 0 (depletion_id=%d)", depletion.Id)
|
|
||||||
if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, 0); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.WithContext(ctx).
|
|
||||||
Table("recording_depletions").
|
|
||||||
Where("id = ?", depletion.Id).
|
|
||||||
Update("usage_qty", 0).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := s.resyncStockableUsageFromAllocations(ctx, tx, fifo.UsableKeyRecordingDepletion, depletion.Id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := s.ensureActiveAllocations(ctx, tx, fifo.UsableKeyRecordingDepletion, depletion.Id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.logDepletionTrace("release:start", depletion, "")
|
s.logDepletionTrace("release:start", depletion, "")
|
||||||
if err := validateDepletionUsage(depletion); err != nil {
|
|
||||||
s.Log.Errorf("FIFO depletion mismatch for recording %d (depletion %d): qty=%.3f usage=%.3f pending=%.3f", depletion.RecordingId, depletion.Id, depletion.Qty, depletion.UsageQty, depletion.PendingQty)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceWarehouseID := uint(0)
|
sourceWarehouseID := uint(0)
|
||||||
if depletion.SourceProductWarehouseId != nil {
|
if depletion.SourceProductWarehouseId != nil {
|
||||||
@@ -438,24 +422,49 @@ func (s *recordingService) releaseRecordingDepletions(
|
|||||||
if sourceWarehouseID == 0 {
|
if sourceWarehouseID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
|
return fiber.NewError(fiber.StatusBadRequest, "Source product warehouse tidak ditemukan untuk depletion")
|
||||||
}
|
}
|
||||||
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
|
|
||||||
UsableKey: recordingDepletionUsableKey,
|
logIncrease := depletion.Qty + depletion.PendingQty
|
||||||
UsableID: depletion.Id,
|
destDelta := depletion.Qty + depletion.PendingQty
|
||||||
Tx: tx,
|
|
||||||
}); err != nil {
|
if err := tx.WithContext(ctx).
|
||||||
s.Log.Errorf("Failed to release FIFO stock for recording depletion %d: %+v", depletion.Id, err)
|
Model(&entity.RecordingDepletion{}).
|
||||||
|
Where("id = ?", depletion.Id).
|
||||||
|
Updates(map[string]any{
|
||||||
|
"qty": 0,
|
||||||
|
"usage_qty": 0,
|
||||||
|
"pending_qty": 0,
|
||||||
|
}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.UpdateDepletionPending(tx, depletion.Id, 0); err != nil {
|
if err := s.reflowRecordingScope(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
sourceWarehouseID,
|
||||||
|
depletion.RecordingId,
|
||||||
|
recordingLaneUsable,
|
||||||
|
recordingFunctionDepletionOut,
|
||||||
|
recordingSourceDepletions,
|
||||||
|
); err != nil {
|
||||||
|
s.Log.Errorf("Failed to reflow FIFO v2 source release for recording depletion %d: %+v", depletion.Id, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if depletion.ProductWarehouseId != 0 {
|
||||||
|
if err := s.reflowRecordingScope(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
depletion.ProductWarehouseId,
|
||||||
|
depletion.RecordingId,
|
||||||
|
recordingLaneStockable,
|
||||||
|
recordingFunctionDepletionIn,
|
||||||
|
recordingSourceDepletions,
|
||||||
|
); err != nil {
|
||||||
|
s.Log.Errorf("Failed to reflow FIFO v2 destination release for recording depletion %d: %+v", depletion.Id, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
s.logDepletionTrace("release:done", depletion, "")
|
s.logDepletionTrace("release:done", depletion, "")
|
||||||
|
|
||||||
logIncrease := depletion.Qty
|
|
||||||
if depletion.PendingQty > 0 {
|
|
||||||
logIncrease += depletion.PendingQty
|
|
||||||
}
|
|
||||||
if logIncrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
if logIncrease > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
||||||
log := &entity.StockLog{
|
log := &entity.StockLog{
|
||||||
ProductWarehouseId: sourceWarehouseID,
|
ProductWarehouseId: sourceWarehouseID,
|
||||||
@@ -482,7 +491,6 @@ func (s *recordingService) releaseRecordingDepletions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
destDelta := depletion.Qty + depletion.PendingQty
|
|
||||||
if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
if depletion.ProductWarehouseId != 0 && destDelta > 0 && strings.TrimSpace(note) != "" && actorID != 0 {
|
||||||
if depletion.ProductWarehouseId == sourceWarehouseID {
|
if depletion.ProductWarehouseId == sourceWarehouseID {
|
||||||
continue
|
continue
|
||||||
@@ -618,9 +626,9 @@ func (s *recordingService) replenishRecordingEggs(
|
|||||||
if len(eggs) == 0 {
|
if len(eggs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if s.FifoSvc == nil {
|
if s.FifoStockV2Svc == nil {
|
||||||
s.Log.Errorf("FIFO service is not available for replenishing recording eggs")
|
s.Log.Errorf("FIFO v2 service is not available for replenishing recording eggs")
|
||||||
return errors.New("fifo service is not available")
|
return errors.New("fifo v2 service is not available")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
|
if strings.TrimSpace(note) != "" && s.StockLogRepo == nil {
|
||||||
return errors.New("stock log repository is not available")
|
return errors.New("stock log repository is not available")
|
||||||
@@ -631,14 +639,23 @@ func (s *recordingService) replenishRecordingEggs(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.logEggTrace("replenish:start", egg, "")
|
s.logEggTrace("replenish:start", egg, "")
|
||||||
if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
|
||||||
StockableKey: fifo.StockableKeyRecordingEgg,
|
if err := tx.WithContext(ctx).
|
||||||
StockableID: egg.Id,
|
Model(&entity.RecordingEgg{}).
|
||||||
ProductWarehouseID: egg.ProductWarehouseId,
|
Where("id = ?", egg.Id).
|
||||||
Quantity: float64(egg.Qty),
|
Update("total_qty", float64(egg.Qty)).Error; err != nil {
|
||||||
Tx: tx,
|
return err
|
||||||
}); err != nil {
|
}
|
||||||
s.Log.Errorf("Failed to replenish FIFO stock for recording egg %d: %+v", egg.Id, err)
|
if err := s.reflowRecordingScope(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
egg.ProductWarehouseId,
|
||||||
|
egg.RecordingId,
|
||||||
|
recordingLaneStockable,
|
||||||
|
recordingFunctionEggIn,
|
||||||
|
recordingSourceEggs,
|
||||||
|
); err != nil {
|
||||||
|
s.Log.Errorf("Failed to reflow FIFO v2 stock for recording egg %d: %+v", egg.Id, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.logEggTrace("replenish:done", egg, "")
|
s.logEggTrace("replenish:done", egg, "")
|
||||||
@@ -681,9 +698,9 @@ func (s *recordingService) replenishRecordingDepletions(
|
|||||||
if len(depletions) == 0 {
|
if len(depletions) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if s.FifoSvc == nil {
|
if s.FifoStockV2Svc == nil {
|
||||||
s.Log.Errorf("FIFO service is not available for replenishing recording depletions")
|
s.Log.Errorf("FIFO v2 service is not available for replenishing recording depletions")
|
||||||
return errors.New("fifo service is not available")
|
return errors.New("fifo v2 service is not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, depletion := range depletions {
|
for _, depletion := range depletions {
|
||||||
@@ -691,14 +708,16 @@ func (s *recordingService) replenishRecordingDepletions(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.logDepletionTrace("replenish:start", depletion, "")
|
s.logDepletionTrace("replenish:start", depletion, "")
|
||||||
if _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
if err := s.reflowRecordingScope(
|
||||||
StockableKey: fifo.StockableKeyRecordingDepletion,
|
ctx,
|
||||||
StockableID: depletion.Id,
|
tx,
|
||||||
ProductWarehouseID: depletion.ProductWarehouseId,
|
depletion.ProductWarehouseId,
|
||||||
Quantity: depletion.Qty,
|
depletion.RecordingId,
|
||||||
Tx: tx,
|
recordingLaneStockable,
|
||||||
}); err != nil {
|
recordingFunctionDepletionIn,
|
||||||
s.Log.Errorf("Failed to replenish FIFO stock for recording depletion %d: %+v", depletion.Id, err)
|
recordingSourceDepletions,
|
||||||
|
); err != nil {
|
||||||
|
s.Log.Errorf("Failed to reflow FIFO v2 stock for recording depletion %d: %+v", depletion.Id, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.logDepletionTrace("replenish:done", depletion, "")
|
s.logDepletionTrace("replenish:done", depletion, "")
|
||||||
@@ -715,9 +734,9 @@ func (s *recordingService) reduceRecordingDepletions(
|
|||||||
if len(depletions) == 0 {
|
if len(depletions) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if s.FifoSvc == nil {
|
if s.FifoStockV2Svc == nil {
|
||||||
s.Log.Errorf("FIFO service is not available for reducing recording depletions")
|
s.Log.Errorf("FIFO v2 service is not available for reducing recording depletions")
|
||||||
return errors.New("fifo service is not available")
|
return errors.New("fifo v2 service is not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, depletion := range depletions {
|
for _, depletion := range depletions {
|
||||||
@@ -725,16 +744,44 @@ func (s *recordingService) reduceRecordingDepletions(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.logDepletionTrace("reduce:start", depletion, "")
|
s.logDepletionTrace("reduce:start", depletion, "")
|
||||||
if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{
|
|
||||||
StockableKey: fifo.StockableKeyRecordingDepletion,
|
if err := tx.WithContext(ctx).
|
||||||
StockableID: depletion.Id,
|
Model(&entity.RecordingDepletion{}).
|
||||||
ProductWarehouseID: depletion.ProductWarehouseId,
|
Where("id = ?", depletion.Id).
|
||||||
Quantity: -depletion.Qty,
|
Updates(map[string]any{
|
||||||
Tx: tx,
|
"qty": 0,
|
||||||
}); err != nil {
|
"usage_qty": 0,
|
||||||
s.Log.Errorf("Failed to reduce FIFO stock for recording depletion %d: %+v", depletion.Id, err)
|
"pending_qty": 0,
|
||||||
|
}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if depletion.SourceProductWarehouseId != nil && *depletion.SourceProductWarehouseId != 0 {
|
||||||
|
if err := s.reflowRecordingScope(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
*depletion.SourceProductWarehouseId,
|
||||||
|
depletion.RecordingId,
|
||||||
|
recordingLaneUsable,
|
||||||
|
recordingFunctionDepletionOut,
|
||||||
|
recordingSourceDepletions,
|
||||||
|
); err != nil {
|
||||||
|
s.Log.Errorf("Failed to reflow FIFO v2 source stock for recording depletion %d: %+v", depletion.Id, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.reflowRecordingScope(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
depletion.ProductWarehouseId,
|
||||||
|
depletion.RecordingId,
|
||||||
|
recordingLaneStockable,
|
||||||
|
recordingFunctionDepletionIn,
|
||||||
|
recordingSourceDepletions,
|
||||||
|
); err != nil {
|
||||||
|
s.Log.Errorf("Failed to reflow FIFO v2 destination stock for recording depletion %d: %+v", depletion.Id, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
s.logDepletionTrace("reduce:done", depletion, "")
|
s.logDepletionTrace("reduce:done", depletion, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,9 +796,9 @@ func (s *recordingService) reduceRecordingEggs(
|
|||||||
if len(eggs) == 0 {
|
if len(eggs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if s.FifoSvc == nil {
|
if s.FifoStockV2Svc == nil {
|
||||||
s.Log.Errorf("FIFO service is not available for reducing recording eggs")
|
s.Log.Errorf("FIFO v2 service is not available for reducing recording eggs")
|
||||||
return errors.New("fifo service is not available")
|
return errors.New("fifo v2 service is not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, egg := range eggs {
|
for _, egg := range eggs {
|
||||||
@@ -759,14 +806,22 @@ func (s *recordingService) reduceRecordingEggs(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.logEggTrace("reduce:start", egg, "")
|
s.logEggTrace("reduce:start", egg, "")
|
||||||
if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{
|
if err := tx.WithContext(ctx).
|
||||||
StockableKey: fifo.StockableKeyRecordingEgg,
|
Model(&entity.RecordingEgg{}).
|
||||||
StockableID: egg.Id,
|
Where("id = ?", egg.Id).
|
||||||
ProductWarehouseID: egg.ProductWarehouseId,
|
Update("total_qty", 0).Error; err != nil {
|
||||||
Quantity: -float64(egg.Qty),
|
return err
|
||||||
Tx: tx,
|
}
|
||||||
}); err != nil {
|
if err := s.reflowRecordingScope(
|
||||||
s.Log.Errorf("Failed to reduce FIFO stock for recording egg %d: %+v", egg.Id, err)
|
ctx,
|
||||||
|
tx,
|
||||||
|
egg.ProductWarehouseId,
|
||||||
|
egg.RecordingId,
|
||||||
|
recordingLaneStockable,
|
||||||
|
recordingFunctionEggIn,
|
||||||
|
recordingSourceEggs,
|
||||||
|
); err != nil {
|
||||||
|
s.Log.Errorf("Failed to reflow FIFO v2 stock for recording egg %d: %+v", egg.Id, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.logEggTrace("reduce:done", egg, "")
|
s.logEggTrace("reduce:done", egg, "")
|
||||||
@@ -934,9 +989,9 @@ func (s *recordingService) syncRecordingStocks(
|
|||||||
note string,
|
note string,
|
||||||
actorID uint,
|
actorID uint,
|
||||||
) error {
|
) error {
|
||||||
if s.FifoSvc == nil {
|
if s.FifoStockV2Svc == nil {
|
||||||
s.Log.Errorf("FIFO service is not available for syncing recording stocks")
|
s.Log.Errorf("FIFO v2 service is not available for syncing recording stocks")
|
||||||
return errors.New("fifo service is not available")
|
return errors.New("fifo v2 service is not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
existingByWarehouse := make(map[uint][]entity.RecordingStock)
|
existingByWarehouse := make(map[uint][]entity.RecordingStock)
|
||||||
@@ -1125,9 +1180,9 @@ func (s *recordingService) rollbackRecordingInventory(ctx context.Context, tx *g
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) requireFIFO() error {
|
func (s *recordingService) requireFIFO() error {
|
||||||
if s.FifoSvc == nil {
|
if s.FifoStockV2Svc == nil {
|
||||||
s.Log.Errorf("FIFO service is not available for recording operations")
|
s.Log.Errorf("FIFO v2 service is not available for recording operations")
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "FIFO service is required for recording operations")
|
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is required for recording operations")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import (
|
|||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,7 +39,6 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
expenseRepository := expenseRepo.NewExpenseRepository(db)
|
expenseRepository := expenseRepo.NewExpenseRepository(db)
|
||||||
expenseRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db)
|
expenseRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db)
|
||||||
projectFlockKandangRepository := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
|
projectFlockKandangRepository := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
|
||||||
stockAllocRepo := commonRepo.NewStockAllocationRepository(db)
|
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
@@ -73,19 +71,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
expenseServiceInstance,
|
expenseServiceInstance,
|
||||||
)
|
)
|
||||||
|
|
||||||
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
|
||||||
_ = fifoService.RegisterStockable(fifo.StockableConfig{
|
|
||||||
Key: fifo.StockableKeyPurchaseItems,
|
|
||||||
Table: "purchase_items",
|
|
||||||
Columns: fifo.StockableColumns{
|
|
||||||
ID: "id",
|
|
||||||
ProductWarehouseID: "product_warehouse_id",
|
|
||||||
TotalQuantity: "total_qty",
|
|
||||||
TotalUsedQuantity: "total_used",
|
|
||||||
CreatedAt: "id",
|
|
||||||
},
|
|
||||||
OrderBy: []string{"id ASC"},
|
|
||||||
})
|
|
||||||
|
|
||||||
purchaseService := service.NewPurchaseService(
|
purchaseService := service.NewPurchaseService(
|
||||||
validate,
|
validate,
|
||||||
@@ -97,7 +83,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
projectFlockKandangRepository,
|
projectFlockKandangRepository,
|
||||||
approvalService,
|
approvalService,
|
||||||
expenseBridge,
|
expenseBridge,
|
||||||
fifoService,
|
fifoStockV2Service,
|
||||||
documentSvc,
|
documentSvc,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
purchaseInFunctionCode = "PURCHASE_IN"
|
||||||
|
purchaseStockableLane = "STOCKABLE"
|
||||||
|
purchaseSourceTable = "purchase_items"
|
||||||
|
)
|
||||||
|
|
||||||
|
func reflowPurchaseScope(
|
||||||
|
ctx context.Context,
|
||||||
|
fifoStockV2Svc commonSvc.FifoStockV2Service,
|
||||||
|
tx *gorm.DB,
|
||||||
|
productWarehouseID uint,
|
||||||
|
asOf *time.Time,
|
||||||
|
) error {
|
||||||
|
if fifoStockV2Svc == nil {
|
||||||
|
return fmt.Errorf("FIFO v2 service is not available")
|
||||||
|
}
|
||||||
|
if productWarehouseID == 0 {
|
||||||
|
return fmt.Errorf("product warehouse id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
flagGroupCode, err := resolvePurchaseFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(flagGroupCode) == "" {
|
||||||
|
return fmt.Errorf("flag group code is not found for product warehouse %d", productWarehouseID)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fifoStockV2Svc.Reflow(ctx, commonSvc.FifoStockV2ReflowRequest{
|
||||||
|
FlagGroupCode: flagGroupCode,
|
||||||
|
ProductWarehouseID: productWarehouseID,
|
||||||
|
AsOf: asOf,
|
||||||
|
Tx: tx,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolvePurchaseFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) {
|
||||||
|
type row struct {
|
||||||
|
FlagGroupCode string `gorm:"column:flag_group_code"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var 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 = ?", purchaseStockableLane).
|
||||||
|
Where("rr.function_code = ?", purchaseInFunctionCode).
|
||||||
|
Where("rr.source_table = ?", purchaseSourceTable).
|
||||||
|
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("rr.id ASC").
|
||||||
|
Limit(1).
|
||||||
|
Take(&selected).Error
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(selected.FlagGroupCode), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func assignEarliestAsOf(m map[uint]time.Time, productWarehouseID uint, asOf time.Time) {
|
||||||
|
if productWarehouseID == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if current, ok := m[productWarehouseID]; !ok || asOf.Before(current) {
|
||||||
|
m[productWarehouseID] = asOf
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ type purchaseService struct {
|
|||||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
ExpenseBridge PurchaseExpenseBridge
|
ExpenseBridge PurchaseExpenseBridge
|
||||||
FifoSvc commonSvc.FifoService
|
FifoStockV2Svc commonSvc.FifoStockV2Service
|
||||||
DocumentSvc commonSvc.DocumentService
|
DocumentSvc commonSvc.DocumentService
|
||||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ func NewPurchaseService(
|
|||||||
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
|
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
|
||||||
approvalSvc commonSvc.ApprovalService,
|
approvalSvc commonSvc.ApprovalService,
|
||||||
expenseBridge PurchaseExpenseBridge,
|
expenseBridge PurchaseExpenseBridge,
|
||||||
fifoSvc commonSvc.FifoService,
|
fifoStockV2Svc commonSvc.FifoStockV2Service,
|
||||||
documentSvc commonSvc.DocumentService,
|
documentSvc commonSvc.DocumentService,
|
||||||
) PurchaseService {
|
) PurchaseService {
|
||||||
return &purchaseService{
|
return &purchaseService{
|
||||||
@@ -91,7 +91,7 @@ func NewPurchaseService(
|
|||||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
ExpenseBridge: expenseBridge,
|
ExpenseBridge: expenseBridge,
|
||||||
FifoSvc: fifoSvc,
|
FifoStockV2Svc: fifoStockV2Svc,
|
||||||
DocumentSvc: documentSvc,
|
DocumentSvc: documentSvc,
|
||||||
approvalWorkflow: utils.ApprovalWorkflowPurchase,
|
approvalWorkflow: utils.ApprovalWorkflowPurchase,
|
||||||
}
|
}
|
||||||
@@ -1026,22 +1026,11 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx)
|
pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx)
|
||||||
stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
|
stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
|
||||||
|
|
||||||
deltas := make(map[uint]float64)
|
|
||||||
affected := make(map[uint]struct{})
|
affected := make(map[uint]struct{})
|
||||||
updates := make([]rPurchase.PurchaseReceivingUpdate, 0, len(prepared))
|
updates := make([]rPurchase.PurchaseReceivingUpdate, 0, len(prepared))
|
||||||
priceUpdates := make([]rPurchase.PurchasePricingUpdate, 0, len(prepared))
|
priceUpdates := make([]rPurchase.PurchasePricingUpdate, 0, len(prepared))
|
||||||
totalQtyDeltas := make(map[uint]float64)
|
totalQtyDeltas := make(map[uint]float64)
|
||||||
fifoAdds := make([]struct {
|
reflowAsOfByPW := make(map[uint]time.Time)
|
||||||
itemID uint
|
|
||||||
pwID uint
|
|
||||||
qty float64
|
|
||||||
}, 0, len(prepared))
|
|
||||||
fifoSubs := make([]struct {
|
|
||||||
itemID uint
|
|
||||||
pwID uint
|
|
||||||
qty float64
|
|
||||||
}, 0, len(prepared))
|
|
||||||
resolvePendingIDs := make(map[uint]struct{})
|
|
||||||
logEntries := make([]struct {
|
logEntries := make([]struct {
|
||||||
itemID uint
|
itemID uint
|
||||||
pwID uint
|
pwID uint
|
||||||
@@ -1083,35 +1072,14 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
delta float64
|
delta float64
|
||||||
}{itemID: item.Id, pwID: *newPWID, delta: deltaQty})
|
}{itemID: item.Id, pwID: *newPWID, delta: deltaQty})
|
||||||
}
|
}
|
||||||
switch {
|
if newPWID != nil {
|
||||||
case deltaQty > 0 && newPWID != nil:
|
assignEarliestAsOf(reflowAsOfByPW, *newPWID, prep.receivedDate.UTC())
|
||||||
if s.FifoSvc != nil {
|
}
|
||||||
fifoAdds = append(fifoAdds, struct {
|
if deltaQty != 0 {
|
||||||
itemID uint
|
|
||||||
pwID uint
|
|
||||||
qty float64
|
|
||||||
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
|
|
||||||
resolvePendingIDs[*newPWID] = struct{}{}
|
|
||||||
} else {
|
|
||||||
deltas[*newPWID] += deltaQty
|
|
||||||
totalQtyDeltas[item.Id] += deltaQty
|
totalQtyDeltas[item.Id] += deltaQty
|
||||||
}
|
}
|
||||||
case deltaQty < 0 && newPWID != nil:
|
if deltaQty < 0 && newPWID != nil {
|
||||||
if s.FifoSvc != nil {
|
|
||||||
fifoSubs = append(fifoSubs, struct {
|
|
||||||
itemID uint
|
|
||||||
pwID uint
|
|
||||||
qty float64
|
|
||||||
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
|
|
||||||
affected[*newPWID] = struct{}{}
|
affected[*newPWID] = struct{}{}
|
||||||
resolvePendingIDs[*newPWID] = struct{}{}
|
|
||||||
} else {
|
|
||||||
deltas[*newPWID] += deltaQty // negative
|
|
||||||
affected[*newPWID] = struct{}{}
|
|
||||||
totalQtyDeltas[item.Id] += deltaQty
|
|
||||||
}
|
|
||||||
case newPWID != nil:
|
|
||||||
resolvePendingIDs[*newPWID] = struct{}{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dateCopy := prep.receivedDate
|
dateCopy := prep.receivedDate
|
||||||
@@ -1147,10 +1115,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := pwRepoTx.AdjustQuantities(c.Context(), deltas, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(priceUpdates) > 0 {
|
if len(priceUpdates) > 0 {
|
||||||
if err := repoTx.UpdatePricing(c.Context(), purchase.Id, priceUpdates); err != nil {
|
if err := repoTx.UpdatePricing(c.Context(), purchase.Id, priceUpdates); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1180,48 +1144,16 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.FifoSvc != nil {
|
if len(reflowAsOfByPW) > 0 {
|
||||||
for _, adj := range fifoAdds {
|
if s.FifoStockV2Svc == nil {
|
||||||
if adj.pwID == 0 || adj.qty <= 0 {
|
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
if _, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
for pwID, asOf := range reflowAsOfByPW {
|
||||||
StockableKey: fifo.StockableKeyPurchaseItems,
|
asOfCopy := asOf
|
||||||
StockableID: adj.itemID,
|
if err := reflowPurchaseScope(c.Context(), s.FifoStockV2Svc, tx, pwID, &asOfCopy); err != nil {
|
||||||
ProductWarehouseID: adj.pwID,
|
|
||||||
Quantity: adj.qty,
|
|
||||||
Tx: tx,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, adj := range fifoSubs {
|
|
||||||
if adj.pwID == 0 || adj.qty >= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := s.FifoSvc.AdjustStockableQuantity(c.Context(), commonSvc.StockAdjustRequest{
|
|
||||||
StockableKey: fifo.StockableKeyPurchaseItems,
|
|
||||||
StockableID: adj.itemID,
|
|
||||||
ProductWarehouseID: adj.pwID,
|
|
||||||
Quantity: adj.qty,
|
|
||||||
Tx: tx,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for pwID := range resolvePendingIDs {
|
|
||||||
if pwID == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
resolved, err := s.FifoSvc.ResolvePending(c.Context(), commonSvc.PendingResolveRequest{
|
|
||||||
ProductWarehouseID: pwID,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.Log.Infof("ResolvePending purchase=%d pw=%d resolved=%d", purchase.Id, pwID, len(resolved))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(logEntries) > 0 {
|
if len(logEntries) > 0 {
|
||||||
@@ -1577,10 +1509,9 @@ func (s *purchaseService) rollbackPurchaseStock(ctx context.Context, tx *gorm.DB
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx)
|
|
||||||
stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
|
stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
|
||||||
deltas := make(map[uint]float64)
|
|
||||||
affected := make(map[uint]struct{})
|
affected := make(map[uint]struct{})
|
||||||
|
reflowAsOfByPW := make(map[uint]time.Time)
|
||||||
logEntries := make([]struct {
|
logEntries := make([]struct {
|
||||||
pwID uint
|
pwID uint
|
||||||
qty float64
|
qty float64
|
||||||
@@ -1596,41 +1527,42 @@ func (s *purchaseService) rollbackPurchaseStock(ctx context.Context, tx *gorm.DB
|
|||||||
pwID := *item.ProductWarehouseId
|
pwID := *item.ProductWarehouseId
|
||||||
qty := item.TotalQty
|
qty := item.TotalQty
|
||||||
|
|
||||||
if s.FifoSvc != nil {
|
if err := tx.WithContext(ctx).
|
||||||
if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{
|
Model(&entity.PurchaseItem{}).
|
||||||
StockableKey: fifo.StockableKeyPurchaseItems,
|
Where("id = ?", item.Id).
|
||||||
StockableID: item.Id,
|
Update("total_qty", 0).Error; err != nil {
|
||||||
ProductWarehouseID: pwID,
|
|
||||||
Quantity: -qty,
|
|
||||||
Tx: tx,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logEntries = append(logEntries, struct {
|
|
||||||
pwID uint
|
|
||||||
qty float64
|
|
||||||
}{pwID: pwID, qty: qty})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
deltas[pwID] -= qty
|
|
||||||
affected[pwID] = struct{}{}
|
affected[pwID] = struct{}{}
|
||||||
|
if item.ReceivedDate != nil {
|
||||||
|
assignEarliestAsOf(reflowAsOfByPW, pwID, item.ReceivedDate.UTC())
|
||||||
|
} else {
|
||||||
|
assignEarliestAsOf(reflowAsOfByPW, pwID, time.Now().UTC())
|
||||||
|
}
|
||||||
logEntries = append(logEntries, struct {
|
logEntries = append(logEntries, struct {
|
||||||
pwID uint
|
pwID uint
|
||||||
qty float64
|
qty float64
|
||||||
}{pwID: pwID, qty: qty})
|
}{pwID: pwID, qty: qty})
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.FifoSvc == nil && len(deltas) > 0 {
|
if len(reflowAsOfByPW) > 0 {
|
||||||
if err := pwRepoTx.AdjustQuantities(ctx, deltas, nil); err != nil {
|
if s.FifoStockV2Svc == nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
|
||||||
|
}
|
||||||
|
for pwID, asOf := range reflowAsOfByPW {
|
||||||
|
asOfCopy := asOf
|
||||||
|
if err := reflowPurchaseScope(ctx, s.FifoStockV2Svc, tx, pwID, &asOfCopy); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(affected) > 0 {
|
if len(affected) > 0 {
|
||||||
if err := pwRepoTx.CleanupEmpty(ctx, affected); err != nil {
|
if err := rProductWarehouse.NewProductWarehouseRepository(tx).CleanupEmpty(ctx, affected); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if strings.TrimSpace(note) != "" && actorID != 0 && len(logEntries) > 0 {
|
if strings.TrimSpace(note) != "" && actorID != 0 && len(logEntries) > 0 {
|
||||||
logs := make([]*entity.StockLog, 0, len(logEntries))
|
logs := make([]*entity.StockLog, 0, len(logEntries))
|
||||||
|
|||||||
Reference in New Issue
Block a user