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:
@@ -23,7 +23,6 @@ import (
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -40,7 +39,6 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
expenseRepository := expenseRepo.NewExpenseRepository(db)
|
||||
expenseRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db)
|
||||
projectFlockKandangRepository := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
|
||||
stockAllocRepo := commonRepo.NewStockAllocationRepository(db)
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
@@ -73,19 +71,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
expenseServiceInstance,
|
||||
)
|
||||
|
||||
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, 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"},
|
||||
})
|
||||
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
|
||||
|
||||
purchaseService := service.NewPurchaseService(
|
||||
validate,
|
||||
@@ -97,7 +83,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
projectFlockKandangRepository,
|
||||
approvalService,
|
||||
expenseBridge,
|
||||
fifoService,
|
||||
fifoStockV2Service,
|
||||
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
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
ExpenseBridge PurchaseExpenseBridge
|
||||
FifoSvc commonSvc.FifoService
|
||||
FifoStockV2Svc commonSvc.FifoStockV2Service
|
||||
DocumentSvc commonSvc.DocumentService
|
||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func NewPurchaseService(
|
||||
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
|
||||
approvalSvc commonSvc.ApprovalService,
|
||||
expenseBridge PurchaseExpenseBridge,
|
||||
fifoSvc commonSvc.FifoService,
|
||||
fifoStockV2Svc commonSvc.FifoStockV2Service,
|
||||
documentSvc commonSvc.DocumentService,
|
||||
) PurchaseService {
|
||||
return &purchaseService{
|
||||
@@ -91,7 +91,7 @@ func NewPurchaseService(
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
ExpenseBridge: expenseBridge,
|
||||
FifoSvc: fifoSvc,
|
||||
FifoStockV2Svc: fifoStockV2Svc,
|
||||
DocumentSvc: documentSvc,
|
||||
approvalWorkflow: utils.ApprovalWorkflowPurchase,
|
||||
}
|
||||
@@ -1026,22 +1026,11 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx)
|
||||
stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
|
||||
|
||||
deltas := make(map[uint]float64)
|
||||
affected := make(map[uint]struct{})
|
||||
updates := make([]rPurchase.PurchaseReceivingUpdate, 0, len(prepared))
|
||||
priceUpdates := make([]rPurchase.PurchasePricingUpdate, 0, len(prepared))
|
||||
totalQtyDeltas := make(map[uint]float64)
|
||||
fifoAdds := make([]struct {
|
||||
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{})
|
||||
reflowAsOfByPW := make(map[uint]time.Time)
|
||||
logEntries := make([]struct {
|
||||
itemID uint
|
||||
pwID uint
|
||||
@@ -1083,35 +1072,14 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
delta float64
|
||||
}{itemID: item.Id, pwID: *newPWID, delta: deltaQty})
|
||||
}
|
||||
switch {
|
||||
case deltaQty > 0 && newPWID != nil:
|
||||
if s.FifoSvc != nil {
|
||||
fifoAdds = append(fifoAdds, struct {
|
||||
itemID uint
|
||||
pwID uint
|
||||
qty float64
|
||||
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
|
||||
resolvePendingIDs[*newPWID] = struct{}{}
|
||||
} else {
|
||||
deltas[*newPWID] += deltaQty
|
||||
totalQtyDeltas[item.Id] += deltaQty
|
||||
}
|
||||
case 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{}{}
|
||||
resolvePendingIDs[*newPWID] = struct{}{}
|
||||
} else {
|
||||
deltas[*newPWID] += deltaQty // negative
|
||||
affected[*newPWID] = struct{}{}
|
||||
totalQtyDeltas[item.Id] += deltaQty
|
||||
}
|
||||
case newPWID != nil:
|
||||
resolvePendingIDs[*newPWID] = struct{}{}
|
||||
if newPWID != nil {
|
||||
assignEarliestAsOf(reflowAsOfByPW, *newPWID, prep.receivedDate.UTC())
|
||||
}
|
||||
if deltaQty != 0 {
|
||||
totalQtyDeltas[item.Id] += deltaQty
|
||||
}
|
||||
if deltaQty < 0 && newPWID != nil {
|
||||
affected[*newPWID] = struct{}{}
|
||||
}
|
||||
|
||||
dateCopy := prep.receivedDate
|
||||
@@ -1147,10 +1115,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
return err
|
||||
}
|
||||
|
||||
if err := pwRepoTx.AdjustQuantities(c.Context(), deltas, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(priceUpdates) > 0 {
|
||||
if err := repoTx.UpdatePricing(c.Context(), purchase.Id, priceUpdates); err != nil {
|
||||
return err
|
||||
@@ -1180,48 +1144,16 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
}
|
||||
}
|
||||
|
||||
if s.FifoSvc != nil {
|
||||
for _, adj := range fifoAdds {
|
||||
if adj.pwID == 0 || adj.qty <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||
StockableKey: fifo.StockableKeyPurchaseItems,
|
||||
StockableID: adj.itemID,
|
||||
ProductWarehouseID: adj.pwID,
|
||||
Quantity: adj.qty,
|
||||
Tx: tx,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(reflowAsOfByPW) > 0 {
|
||||
if s.FifoStockV2Svc == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
|
||||
}
|
||||
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 {
|
||||
for pwID, asOf := range reflowAsOfByPW {
|
||||
asOfCopy := asOf
|
||||
if err := reflowPurchaseScope(c.Context(), s.FifoStockV2Svc, tx, pwID, &asOfCopy); 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 {
|
||||
@@ -1577,10 +1509,9 @@ func (s *purchaseService) rollbackPurchaseStock(ctx context.Context, tx *gorm.DB
|
||||
return nil
|
||||
}
|
||||
|
||||
pwRepoTx := rProductWarehouse.NewProductWarehouseRepository(tx)
|
||||
stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
|
||||
deltas := make(map[uint]float64)
|
||||
affected := make(map[uint]struct{})
|
||||
reflowAsOfByPW := make(map[uint]time.Time)
|
||||
logEntries := make([]struct {
|
||||
pwID uint
|
||||
qty float64
|
||||
@@ -1596,42 +1527,43 @@ func (s *purchaseService) rollbackPurchaseStock(ctx context.Context, tx *gorm.DB
|
||||
pwID := *item.ProductWarehouseId
|
||||
qty := item.TotalQty
|
||||
|
||||
if s.FifoSvc != nil {
|
||||
if err := s.FifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{
|
||||
StockableKey: fifo.StockableKeyPurchaseItems,
|
||||
StockableID: item.Id,
|
||||
ProductWarehouseID: pwID,
|
||||
Quantity: -qty,
|
||||
Tx: tx,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
logEntries = append(logEntries, struct {
|
||||
pwID uint
|
||||
qty float64
|
||||
}{pwID: pwID, qty: qty})
|
||||
continue
|
||||
if err := tx.WithContext(ctx).
|
||||
Model(&entity.PurchaseItem{}).
|
||||
Where("id = ?", item.Id).
|
||||
Update("total_qty", 0).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deltas[pwID] -= qty
|
||||
affected[pwID] = struct{}{}
|
||||
if item.ReceivedDate != nil {
|
||||
assignEarliestAsOf(reflowAsOfByPW, pwID, item.ReceivedDate.UTC())
|
||||
} else {
|
||||
assignEarliestAsOf(reflowAsOfByPW, pwID, time.Now().UTC())
|
||||
}
|
||||
logEntries = append(logEntries, struct {
|
||||
pwID uint
|
||||
qty float64
|
||||
}{pwID: pwID, qty: qty})
|
||||
}
|
||||
|
||||
if s.FifoSvc == nil && len(deltas) > 0 {
|
||||
if err := pwRepoTx.AdjustQuantities(ctx, deltas, nil); err != nil {
|
||||
return err
|
||||
if len(reflowAsOfByPW) > 0 {
|
||||
if s.FifoStockV2Svc == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
|
||||
}
|
||||
if len(affected) > 0 {
|
||||
if err := pwRepoTx.CleanupEmpty(ctx, affected); err != nil {
|
||||
for pwID, asOf := range reflowAsOfByPW {
|
||||
asOfCopy := asOf
|
||||
if err := reflowPurchaseScope(ctx, s.FifoStockV2Svc, tx, pwID, &asOfCopy); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(affected) > 0 {
|
||||
if err := rProductWarehouse.NewProductWarehouseRepository(tx).CleanupEmpty(ctx, affected); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(note) != "" && actorID != 0 && len(logEntries) > 0 {
|
||||
logs := make([]*entity.StockLog, 0, len(logEntries))
|
||||
for _, entry := range logEntries {
|
||||
|
||||
Reference in New Issue
Block a user