Feat[BE]: implement FIFO stock management for marketing delivery products, including migration scripts and updates to related services and DTOs

This commit is contained in:
aguhh18
2025-12-26 19:02:50 +07:00
parent 54487b0fcf
commit cd14de4dd2
16 changed files with 290 additions and 72 deletions
@@ -0,0 +1,28 @@
-- ============================================
-- Rollback: Remove FIFO fields and restore qty column
-- ============================================
-- STEP 1: Drop indexes
DROP INDEX IF EXISTS idx_marketing_delivery_products_fifo_lookup;
DROP INDEX IF EXISTS idx_marketing_delivery_products_pending_qty;
DROP INDEX IF EXISTS idx_marketing_delivery_products_usage_qty;
DROP INDEX IF EXISTS idx_marketing_delivery_products_created_at;
-- STEP 2: Drop constraints
ALTER TABLE marketing_delivery_products
DROP CONSTRAINT IF EXISTS chk_marketing_delivery_products_fifo_nonneg;
-- STEP 3: Restore qty column from usage_qty data
ALTER TABLE marketing_delivery_products
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) DEFAULT 0 NOT NULL;
-- Migrate data back from usage_qty to qty
UPDATE marketing_delivery_products
SET qty = usage_qty
WHERE qty = 0;
-- STEP 4: Drop FIFO columns
ALTER TABLE marketing_delivery_products
DROP COLUMN IF EXISTS usage_qty,
DROP COLUMN IF EXISTS pending_qty,
DROP COLUMN IF EXISTS created_at;
@@ -0,0 +1,58 @@
-- ============================================
-- Add FIFO fields to marketing_delivery_products
-- This migration adds fields needed for FIFO stock management
-- and removes the old qty field in favor of FIFO-based allocation
-- ============================================
-- STEP 0: Drop orphan indexes from previous migration
DROP INDEX IF EXISTS idx_marketing_delivery_products_deleted_at;
-- STEP 1: Add created_at column (required for FIFO ordering)
ALTER TABLE marketing_delivery_products
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
-- STEP 2: Add FIFO tracking fields
ALTER TABLE marketing_delivery_products
ADD COLUMN IF NOT EXISTS usage_qty NUMERIC(15, 3) DEFAULT 0,
ADD COLUMN IF NOT EXISTS pending_qty NUMERIC(15, 3) DEFAULT 0;
-- STEP 3: Migrate data from old qty to usage_qty for existing records
-- This preserves existing quantity data as allocated quantity
UPDATE marketing_delivery_products
SET
usage_qty = COALESCE(qty, 0),
pending_qty = 0
WHERE usage_qty = 0;
-- STEP 4: Drop the old qty column (replaced by usage_qty + pending_qty)
ALTER TABLE marketing_delivery_products
DROP COLUMN IF EXISTS qty;
-- STEP 5: Make FIFO fields NOT NULL
ALTER TABLE marketing_delivery_products
ALTER COLUMN usage_qty SET NOT NULL,
ALTER COLUMN pending_qty SET NOT NULL,
ALTER COLUMN created_at SET NOT NULL;
-- STEP 6: Add constraints to ensure non-negative values
ALTER TABLE marketing_delivery_products
ADD CONSTRAINT chk_marketing_delivery_products_fifo_nonneg CHECK (
usage_qty >= 0 AND
pending_qty >= 0
);
-- STEP 7: Create indexes for FIFO operations
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_created_at
ON marketing_delivery_products(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_usage_qty
ON marketing_delivery_products(usage_qty)
WHERE usage_qty > 0;
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_pending_qty
ON marketing_delivery_products(pending_qty)
WHERE pending_qty > 0;
-- Composite index for FIFO lookups
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_fifo_lookup
ON marketing_delivery_products(marketing_product_id, created_at DESC);
@@ -0,0 +1,7 @@
-- Remove foreign key constraint
ALTER TABLE marketing_delivery_products
DROP CONSTRAINT IF EXISTS fk_marketing_delivery_products_product_warehouse;
-- Drop product_warehouse_id column
ALTER TABLE marketing_delivery_products
DROP COLUMN IF EXISTS product_warehouse_id;
@@ -0,0 +1,19 @@
-- Add product_warehouse_id column to marketing_delivery_products
ALTER TABLE marketing_delivery_products
ADD COLUMN IF NOT EXISTS product_warehouse_id INT NOT NULL DEFAULT 0;
-- Fill product_warehouse_id from marketing_products
UPDATE marketing_delivery_products mdp
SET product_warehouse_id = mp.product_warehouse_id
FROM marketing_products mp
WHERE mdp.marketing_product_id = mp.id
AND mdp.product_warehouse_id = 0;
-- Set NOT NULL constraint
ALTER TABLE marketing_delivery_products
ALTER COLUMN product_warehouse_id SET NOT NULL;
-- Add foreign key constraint
ALTER TABLE marketing_delivery_products
ADD CONSTRAINT fk_marketing_delivery_products_product_warehouse
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id);
@@ -5,15 +5,20 @@ import (
) )
type MarketingDeliveryProduct struct { type MarketingDeliveryProduct struct {
Id uint `gorm:"primaryKey;autoIncrement"` Id uint `gorm:"primaryKey;autoIncrement"`
MarketingProductId uint `gorm:"uniqueIndex;not null"` MarketingProductId uint `gorm:"uniqueIndex;not null"`
Qty float64 `gorm:"type:numeric(15,3)"` ProductWarehouseId uint `gorm:"not null"`
UnitPrice float64 `gorm:"type:numeric(15,3)"` UnitPrice float64 `gorm:"type:numeric(15,3)"`
TotalWeight float64 `gorm:"type:numeric(15,3)"` TotalWeight float64 `gorm:"type:numeric(15,3)"`
AvgWeight float64 `gorm:"type:numeric(15,3)"` AvgWeight float64 `gorm:"type:numeric(15,3)"`
TotalPrice float64 `gorm:"type:numeric(15,3)"` TotalPrice float64 `gorm:"type:numeric(15,3)"`
DeliveryDate *time.Time `gorm:"type:timestamptz"` DeliveryDate *time.Time `gorm:"type:timestamptz"`
VehicleNumber string `gorm:"type:varchar(50)"` VehicleNumber string `gorm:"type:varchar(50)"`
// FIFO Fields
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
PendingQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
CreatedAt *time.Time `gorm:"type:timestamptz;not null"`
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"` MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
} }
+1
View File
@@ -77,6 +77,7 @@ const (
P_DeliveryGetAll = "lti.marketing.delivery_order.list" P_DeliveryGetAll = "lti.marketing.delivery_order.list"
P_DeliveryGetOne = "lti.marketing.delivery_order.detail" P_DeliveryGetOne = "lti.marketing.delivery_order.detail"
P_DeliveryUpdateOne = "lti.marketing.delivery_order.update" P_DeliveryUpdateOne = "lti.marketing.delivery_order.update"
P_DeliveryCreateOne = "lti.marketing.delivery_order.Create"
P_SalesOrderDelete = "lti.marketing.sales_order.delete" P_SalesOrderDelete = "lti.marketing.sales_order.delete"
P_SalesOrderApproval = "lti.marketing.sales_order.approve" P_SalesOrderApproval = "lti.marketing.sales_order.approve"
P_SalesOrderCreateOne = "lti.marketing.sales_order.create" P_SalesOrderCreateOne = "lti.marketing.sales_order.create"
@@ -64,7 +64,7 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
DoNumber: doNumber, DoNumber: doNumber,
Product: product, Product: product,
Customer: customer, Customer: customer,
Qty: e.Qty, Qty: e.UsageQty, // Show allocated quantity from FIFO
Weight: e.TotalWeight, Weight: e.TotalWeight,
AvgWeight: e.AvgWeight, AvgWeight: e.AvgWeight,
Price: e.UnitPrice, Price: e.UnitPrice,
@@ -712,13 +712,13 @@ func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlo
) )
for _, product := range deliveryProducts { for _, product := range deliveryProducts {
if product.Qty == 0 { if product.UsageQty == 0 {
continue continue
} }
projectFlockKandang := product.MarketingProduct.ProductWarehouse.ProjectFlockKandang projectFlockKandang := product.MarketingProduct.ProductWarehouse.ProjectFlockKandang
ageWeeks := dto.CalculateAgeFromChickinDataProduksi(projectFlockKandang, product.DeliveryDate) ageWeeks := dto.CalculateAgeFromChickinDataProduksi(projectFlockKandang, product.DeliveryDate)
totalAgeWeeks += float64(ageWeeks) * product.Qty totalAgeWeeks += float64(ageWeeks) * product.UsageQty
totalQty += product.Qty totalQty += product.UsageQty
} }
if totalQty == 0 { if totalQty == 0 {
@@ -121,7 +121,7 @@ func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingD
return MarketingDeliveryProductDTO{ return MarketingDeliveryProductDTO{
Id: e.Id, Id: e.Id,
MarketingProductId: e.MarketingProductId, MarketingProductId: e.MarketingProductId,
Qty: e.Qty, Qty: e.UsageQty,
UnitPrice: e.UnitPrice, UnitPrice: e.UnitPrice,
TotalWeight: e.TotalWeight, TotalWeight: e.TotalWeight,
AvgWeight: e.AvgWeight, AvgWeight: e.AvgWeight,
+29 -4
View File
@@ -2,6 +2,7 @@ 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"
@@ -13,11 +14,12 @@ import (
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services" service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
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{}
@@ -31,6 +33,28 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
customerRepo := rCustomer.NewCustomerRepository(db) customerRepo := rCustomer.NewCustomerRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
// Initialize FIFO service
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
// Register marketing_delivery_products as FIFO Usable
// Note: ProductWarehouseID comes from marketing_products table via preload
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyMarketingDelivery,
Table: "marketing_delivery_products",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id", // Resolved from marketing_products via preload
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))
}
}
// Initialize approval service // Initialize approval service
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalSvc := commonSvc.NewApprovalService(approvalRepo) approvalSvc := commonSvc.NewApprovalService(approvalRepo)
@@ -42,9 +66,10 @@ 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)
// Initialize services // Initialize services
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc,warehouseRepo,projectFlockKandangRepo, validate) salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, warehouseRepo, projectFlockKandangRepo, validate)
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate) deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, fifoService, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
// Register routes // Register routes
@@ -17,6 +17,9 @@ type MarketingDeliveryProductRepository interface {
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error)
UpdateFifoFields(ctx context.Context, id uint, usageQty, pendingQty float64) error
GetUsageQty(ctx context.Context, id uint) (float64, error)
ResetFifoFields(ctx context.Context, id uint) error
} }
type MarketingDeliveryProductRepositoryImpl struct { type MarketingDeliveryProductRepositoryImpl struct {
@@ -241,3 +244,33 @@ func containsJoin(db *gorm.DB, tableName string) bool {
joinSQL := statement.SQL.String() joinSQL := statement.SQL.String()
return strings.Contains(joinSQL, "JOIN "+tableName) return strings.Contains(joinSQL, "JOIN "+tableName)
} }
func (r *MarketingDeliveryProductRepositoryImpl) UpdateFifoFields(ctx context.Context, id uint, usageQty, pendingQty float64) error {
return r.DB().WithContext(ctx).
Model(&entity.MarketingDeliveryProduct{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"usage_qty": usageQty,
"pending_qty": pendingQty,
}).Error
}
func (r *MarketingDeliveryProductRepositoryImpl) GetUsageQty(ctx context.Context, id uint) (float64, error) {
var usageQty float64
err := r.DB().WithContext(ctx).
Model(&entity.MarketingDeliveryProduct{}).
Where("id = ?", id).
Select("usage_qty").
Scan(&usageQty).Error
return usageQty, err
}
func (r *MarketingDeliveryProductRepositoryImpl) ResetFifoFields(ctx context.Context, id uint) error {
return r.DB().WithContext(ctx).
Model(&entity.MarketingDeliveryProduct{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"usage_qty": 0,
"pending_qty": 0,
}).Error
}
+9 -6
View File
@@ -16,12 +16,15 @@ func RegisterRoutes(router fiber.Router, userService user.UserService, salesOrde
route := router.Group("/marketing") route := router.Group("/marketing")
route.Use(m.Auth(userService)) route.Use(m.Auth(userService))
route.Get("/",m.RequirePermissions(m.P_DeliveryGetAll), deliveryOrdersCtrl.GetAll) route.Get("/", m.RequirePermissions(m.P_DeliveryGetAll), deliveryOrdersCtrl.GetAll)
route.Get("/:id",m.RequirePermissions(m.P_DeliveryGetOne), deliveryOrdersCtrl.GetOne) route.Get("/:id", m.RequirePermissions(m.P_DeliveryGetOne), deliveryOrdersCtrl.GetOne)
route.Delete("/:id",m.RequirePermissions(m.P_SalesOrderDelete), salesOrdersCtrl.DeleteOne) route.Delete("/:id", m.RequirePermissions(m.P_SalesOrderDelete), salesOrdersCtrl.DeleteOne)
route.Post("/sales-orders",m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne) route.Post("/sales-orders", m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne)
route.Patch("/sales-orders/:id",m.RequirePermissions(m.P_SalesOrderUpdateOne), salesOrdersCtrl.UpdateOne) route.Patch("/sales-orders/:id", m.RequirePermissions(m.P_SalesOrderUpdateOne), salesOrdersCtrl.UpdateOne)
route.Post("/sales-orders/approvals",m.RequirePermissions(m.P_SalesOrderApproval), salesOrdersCtrl.Approval) route.Post("/sales-orders/approvals", m.RequirePermissions(m.P_SalesOrderApproval), salesOrdersCtrl.Approval)
route.Post("/delivery-orders", m.RequirePermissions(m.P_DeliveryCreateOne), deliveryOrdersCtrl.CreateOne)
route.Patch("/delivery-orders/:id", m.RequirePermissions(m.P_DeliveryUpdateOne), deliveryOrdersCtrl.UpdateOne)
} }
@@ -15,10 +15,10 @@ import (
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
"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"
"github.com/sirupsen/logrus"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -30,12 +30,12 @@ type DeliveryOrdersService interface {
} }
type deliveryOrdersService struct { type deliveryOrdersService struct {
Log *logrus.Logger
Validate *validator.Validate Validate *validator.Validate
MarketingRepo marketingRepo.MarketingRepository MarketingRepo marketingRepo.MarketingRepository
MarketingProductRepo marketingRepo.MarketingProductRepository MarketingProductRepo marketingRepo.MarketingProductRepository
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
FifoSvc commonSvc.FifoService
} }
func NewDeliveryOrdersService( func NewDeliveryOrdersService(
@@ -43,15 +43,16 @@ func NewDeliveryOrdersService(
marketingProductRepo marketingRepo.MarketingProductRepository, marketingProductRepo marketingRepo.MarketingProductRepository,
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository, marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
approvalSvc commonSvc.ApprovalService, approvalSvc commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService,
validate *validator.Validate, validate *validator.Validate,
) DeliveryOrdersService { ) DeliveryOrdersService {
return &deliveryOrdersService{ return &deliveryOrdersService{
Log: utils.Log,
Validate: validate, Validate: validate,
MarketingRepo: marketingRepo, MarketingRepo: marketingRepo,
MarketingProductRepo: marketingProductRepo, MarketingProductRepo: marketingProductRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
FifoSvc: fifoSvc,
} }
} }
@@ -108,7 +109,6 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
}) })
if err != nil { if err != nil {
s.Log.Errorf("Failed to get marketings: %+v", err)
return nil, 0, err return nil, 0, err
} }
for i := range marketings { for i := range marketings {
@@ -116,7 +116,7 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
return db.Preload("ActionUser") return db.Preload("ActionUser")
}) })
if err != nil { if err != nil {
s.Log.Warnf("Failed to load approval for marketing %d: %+v", marketings[i].Id, err) continue
} }
marketings[i].LatestApproval = latestApproval marketings[i].LatestApproval = latestApproval
} }
@@ -247,7 +247,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
itemDeliveryDate = &parsedDate itemDeliveryDate = &parsedDate
} }
deliveryProduct.Qty = requestedProduct.Qty deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.UnitPrice = requestedProduct.UnitPrice
deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.AvgWeight = requestedProduct.AvgWeight
deliveryProduct.TotalWeight = requestedProduct.TotalWeight deliveryProduct.TotalWeight = requestedProduct.TotalWeight
@@ -256,7 +256,8 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
if requestedProduct.Qty > 0 { if requestedProduct.Qty > 0 {
if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, requestedProduct.Qty); err != nil {
if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty); err != nil {
return err return err
} }
} }
@@ -354,8 +355,9 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
itemDeliveryDate = deliveryProduct.DeliveryDate itemDeliveryDate = deliveryProduct.DeliveryDate
} }
oldQty := deliveryProduct.Qty oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
deliveryProduct.Qty = requestedProduct.Qty
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.UnitPrice = requestedProduct.UnitPrice
deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.AvgWeight = requestedProduct.AvgWeight
deliveryProduct.TotalWeight = requestedProduct.TotalWeight deliveryProduct.TotalWeight = requestedProduct.TotalWeight
@@ -363,14 +365,18 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.DeliveryDate = itemDeliveryDate
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
qtyChange := requestedProduct.Qty - oldQty if requestedProduct.Qty != oldRequestedQty {
if qtyChange > 0 {
if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, qtyChange); err != nil { if oldRequestedQty > 0 {
return err if err := s.releaseDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct); err != nil {
return err
}
} }
} else if qtyChange < 0 {
if err := s.restoreProductWarehouseStock(c.Context(), dbTransaction, foundMarketingProduct, -qtyChange); err != nil { if requestedProduct.Qty > 0 {
return err if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty); err != nil {
return err
}
} }
} }
@@ -393,50 +399,79 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
return s.getMarketingWithDeliveries(c, id) return s.getMarketingWithDeliveries(c, id)
} }
func (s deliveryOrdersService) validateAndReduceProductWarehouse(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyDeliver float64) error { func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, requestedQty float64) error {
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
} }
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) if deliveryProduct == nil || deliveryProduct.Id == 0 {
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,
})
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found") pw, err2 := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
if err2 != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check product warehouse stock")
} }
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock")
if pw == nil || pw.Quantity < requestedQty {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. Available: %.2f, Requested: %.2f", func() float64 { if pw != nil { return pw.Quantity } else { return 0 } }(), requestedQty))
}
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, requestedQty, 0); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
}
return nil
} }
if pw.Quantity < qtyDeliver { if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for warehouse - available: %.2f, requested: %.2f", pw.Quantity, qtyDeliver)) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
} }
pw.Quantity = pw.Quantity - qtyDeliver
if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock")
}
return nil return nil
} }
func (s deliveryOrdersService) restoreProductWarehouseStock(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyRestore float64) error { func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct) error {
if deliveryProduct == nil || deliveryProduct.Id == 0 {
return nil
}
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
} }
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil) currentUsage, err := deliveryProductRepo.GetUsageQty(ctx, deliveryProduct.Id)
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { currentUsage = 0
return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock")
} }
pw.Quantity = pw.Quantity + qtyRestore if currentUsage == 0 {
if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil { return nil
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock") }
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
Tx: tx,
}); err != nil {
return err
}
if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil {
return err
} }
return nil return nil
@@ -307,15 +307,17 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil { if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
mdp := &entity.MarketingDeliveryProduct{ mdp := &entity.MarketingDeliveryProduct{
MarketingProductId: old.Id, MarketingProductId: old.Id,
Qty: 0,
UnitPrice: 0, UnitPrice: 0,
TotalWeight: 0, TotalWeight: 0,
AvgWeight: 0, AvgWeight: 0,
TotalPrice: 0, TotalPrice: 0,
DeliveryDate: nil, DeliveryDate: nil,
VehicleNumber: rp.VehicleNumber, VehicleNumber: rp.VehicleNumber,
UsageQty: 0,
PendingQty: 0,
} }
if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil { if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product")
@@ -340,7 +342,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
} }
if err == nil { if err == nil {
if deliveryProduct.DeliveryDate != nil || deliveryProduct.Qty > 0 { if deliveryProduct.DeliveryDate != nil || deliveryProduct.UsageQty > 0 || deliveryProduct.PendingQty > 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id))
} }
@@ -602,13 +604,14 @@ func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Cont
marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ marketingDeliveryProduct := &entity.MarketingDeliveryProduct{
MarketingProductId: marketingProduct.Id, MarketingProductId: marketingProduct.Id,
Qty: 0,
UnitPrice: 0, UnitPrice: 0,
TotalWeight: 0, TotalWeight: 0,
AvgWeight: 0, AvgWeight: 0,
TotalPrice: 0, TotalPrice: 0,
DeliveryDate: nil, DeliveryDate: nil,
VehicleNumber: rp.VehicleNumber, VehicleNumber: rp.VehicleNumber,
UsageQty: 0,
PendingQty: 0,
} }
if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil { if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil {
return err return err
@@ -61,7 +61,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
doNumber := marketingDTO.GenerateDeliveryOrderNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId) doNumber := marketingDTO.GenerateDeliveryOrderNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId)
totalWeightKg := mdp.Qty * mdp.AvgWeight totalWeightKg := mdp.UsageQty * mdp.AvgWeight
salesAmount := totalWeightKg * mdp.UnitPrice salesAmount := totalWeightKg * mdp.UnitPrice
var hpp float64 var hpp float64
@@ -78,7 +78,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
AgingDays: agingDays, AgingDays: agingDays,
DoNumber: doNumber, DoNumber: doNumber,
MarketingType: getMarketingType(mdp), MarketingType: getMarketingType(mdp),
Qty: mdp.Qty, Qty: mdp.UsageQty,
AverageWeightKg: mdp.AvgWeight, AverageWeightKg: mdp.AvgWeight,
TotalWeightKg: totalWeightKg, TotalWeightKg: totalWeightKg,
SalesPricePerKg: mdp.UnitPrice, SalesPricePerKg: mdp.UnitPrice,
@@ -194,8 +194,8 @@ func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, ca
totalHppAmount := int64(0) totalHppAmount := int64(0)
for _, mdp := range mdps { for _, mdp := range mdps {
calculatedTotalWeight := mdp.Qty * mdp.AvgWeight calculatedTotalWeight := mdp.UsageQty * mdp.AvgWeight
totalQty += int(mdp.Qty) totalQty += int(mdp.UsageQty)
totalWeightKg += calculatedTotalWeight totalWeightKg += calculatedTotalWeight
totalSalesAmount += int64(calculatedTotalWeight * mdp.UnitPrice) totalSalesAmount += int64(calculatedTotalWeight * mdp.UnitPrice)
+3 -2
View File
@@ -1,6 +1,7 @@
package fifo package fifo
const ( const (
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK" UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN" UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
) )