diff --git a/internal/database/migrations/20251226054852_add_fifo_fields_to_marketing_delivery_products.down.sql b/internal/database/migrations/20251226054852_add_fifo_fields_to_marketing_delivery_products.down.sql new file mode 100644 index 00000000..b9637077 --- /dev/null +++ b/internal/database/migrations/20251226054852_add_fifo_fields_to_marketing_delivery_products.down.sql @@ -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; diff --git a/internal/database/migrations/20251226054852_add_fifo_fields_to_marketing_delivery_products.up.sql b/internal/database/migrations/20251226054852_add_fifo_fields_to_marketing_delivery_products.up.sql new file mode 100644 index 00000000..160c1f05 --- /dev/null +++ b/internal/database/migrations/20251226054852_add_fifo_fields_to_marketing_delivery_products.up.sql @@ -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); diff --git a/internal/database/migrations/20251226114218_add.down.sql b/internal/database/migrations/20251226114218_add.down.sql new file mode 100644 index 00000000..15f3368a --- /dev/null +++ b/internal/database/migrations/20251226114218_add.down.sql @@ -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; diff --git a/internal/database/migrations/20251226114218_add.up.sql b/internal/database/migrations/20251226114218_add.up.sql new file mode 100644 index 00000000..97e5b4ee --- /dev/null +++ b/internal/database/migrations/20251226114218_add.up.sql @@ -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); diff --git a/internal/entities/marketing_delivery_product.go b/internal/entities/marketing_delivery_product.go index 47f6e89d..7ac3d967 100644 --- a/internal/entities/marketing_delivery_product.go +++ b/internal/entities/marketing_delivery_product.go @@ -5,15 +5,20 @@ import ( ) type MarketingDeliveryProduct struct { - Id uint `gorm:"primaryKey;autoIncrement"` - MarketingProductId uint `gorm:"uniqueIndex;not null"` - Qty float64 `gorm:"type:numeric(15,3)"` - UnitPrice float64 `gorm:"type:numeric(15,3)"` - TotalWeight float64 `gorm:"type:numeric(15,3)"` - AvgWeight float64 `gorm:"type:numeric(15,3)"` - TotalPrice float64 `gorm:"type:numeric(15,3)"` - DeliveryDate *time.Time `gorm:"type:timestamptz"` - VehicleNumber string `gorm:"type:varchar(50)"` + Id uint `gorm:"primaryKey;autoIncrement"` + MarketingProductId uint `gorm:"uniqueIndex;not null"` + ProductWarehouseId uint `gorm:"not null"` + UnitPrice float64 `gorm:"type:numeric(15,3)"` + TotalWeight float64 `gorm:"type:numeric(15,3)"` + AvgWeight float64 `gorm:"type:numeric(15,3)"` + TotalPrice float64 `gorm:"type:numeric(15,3)"` + DeliveryDate *time.Time `gorm:"type:timestamptz"` + 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"` } diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index 02145930..7a21262c 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -77,6 +77,7 @@ const ( P_DeliveryGetAll = "lti.marketing.delivery_order.list" P_DeliveryGetOne = "lti.marketing.delivery_order.detail" P_DeliveryUpdateOne = "lti.marketing.delivery_order.update" + P_DeliveryCreateOne = "lti.marketing.delivery_order.Create" P_SalesOrderDelete = "lti.marketing.sales_order.delete" P_SalesOrderApproval = "lti.marketing.sales_order.approve" P_SalesOrderCreateOne = "lti.marketing.sales_order.create" diff --git a/internal/modules/closings/dto/closingMarketing.dto.go b/internal/modules/closings/dto/closingMarketing.dto.go index 8c904561..4c7b4d35 100644 --- a/internal/modules/closings/dto/closingMarketing.dto.go +++ b/internal/modules/closings/dto/closingMarketing.dto.go @@ -64,7 +64,7 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO { DoNumber: doNumber, Product: product, Customer: customer, - Qty: e.Qty, + Qty: e.UsageQty, // Show allocated quantity from FIFO Weight: e.TotalWeight, AvgWeight: e.AvgWeight, Price: e.UnitPrice, diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 48728195..ab8e6f7b 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -712,13 +712,13 @@ func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlo ) for _, product := range deliveryProducts { - if product.Qty == 0 { + if product.UsageQty == 0 { continue } projectFlockKandang := product.MarketingProduct.ProductWarehouse.ProjectFlockKandang ageWeeks := dto.CalculateAgeFromChickinDataProduksi(projectFlockKandang, product.DeliveryDate) - totalAgeWeeks += float64(ageWeeks) * product.Qty - totalQty += product.Qty + totalAgeWeeks += float64(ageWeeks) * product.UsageQty + totalQty += product.UsageQty } if totalQty == 0 { diff --git a/internal/modules/marketing/dto/deliveryorder.dto.go b/internal/modules/marketing/dto/deliveryorder.dto.go index b2bb70d7..a6eea180 100644 --- a/internal/modules/marketing/dto/deliveryorder.dto.go +++ b/internal/modules/marketing/dto/deliveryorder.dto.go @@ -121,7 +121,7 @@ func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingD return MarketingDeliveryProductDTO{ Id: e.Id, MarketingProductId: e.MarketingProductId, - Qty: e.Qty, + Qty: e.UsageQty, UnitPrice: e.UnitPrice, TotalWeight: e.TotalWeight, AvgWeight: e.AvgWeight, diff --git a/internal/modules/marketing/module.go b/internal/modules/marketing/module.go index 586e7961..d8c8fc6a 100644 --- a/internal/modules/marketing/module.go +++ b/internal/modules/marketing/module.go @@ -2,6 +2,7 @@ package marketing import ( "fmt" + "strings" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -13,11 +14,12 @@ import ( repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services" 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" 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/fifo" ) type MarketingModule struct{} @@ -31,6 +33,28 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate customerRepo := rCustomer.NewCustomerRepository(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 approvalRepo := commonRepo.NewApprovalRepository(db) approvalSvc := commonSvc.NewApprovalService(approvalRepo) @@ -42,9 +66,10 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate warehouseRepo := rWarehouse.NewWarehouseRepository(db) projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) + // Initialize services - salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc,warehouseRepo,projectFlockKandangRepo, validate) - deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate) + salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, warehouseRepo, projectFlockKandangRepo, validate) + deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, fifoService, validate) userService := sUser.NewUserService(userRepo, validate) // Register routes diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index ba2c1133..04051009 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -17,6 +17,9 @@ type MarketingDeliveryProductRepository interface { GetByMarketingId(ctx context.Context, marketingId 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) + 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 { @@ -241,3 +244,33 @@ func containsJoin(db *gorm.DB, tableName string) bool { joinSQL := statement.SQL.String() 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 +} diff --git a/internal/modules/marketing/route.go b/internal/modules/marketing/route.go index 139d1ee9..81402c7c 100644 --- a/internal/modules/marketing/route.go +++ b/internal/modules/marketing/route.go @@ -16,12 +16,15 @@ func RegisterRoutes(router fiber.Router, userService user.UserService, salesOrde route := router.Group("/marketing") route.Use(m.Auth(userService)) - route.Get("/",m.RequirePermissions(m.P_DeliveryGetAll), deliveryOrdersCtrl.GetAll) - route.Get("/:id",m.RequirePermissions(m.P_DeliveryGetOne), deliveryOrdersCtrl.GetOne) - route.Delete("/:id",m.RequirePermissions(m.P_SalesOrderDelete), salesOrdersCtrl.DeleteOne) + route.Get("/", m.RequirePermissions(m.P_DeliveryGetAll), deliveryOrdersCtrl.GetAll) + route.Get("/:id", m.RequirePermissions(m.P_DeliveryGetOne), deliveryOrdersCtrl.GetOne) + route.Delete("/:id", m.RequirePermissions(m.P_SalesOrderDelete), salesOrdersCtrl.DeleteOne) - route.Post("/sales-orders",m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne) - 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", m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne) + 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("/delivery-orders", m.RequirePermissions(m.P_DeliveryCreateOne), deliveryOrdersCtrl.CreateOne) + route.Patch("/delivery-orders/:id", m.RequirePermissions(m.P_DeliveryUpdateOne), deliveryOrdersCtrl.UpdateOne) } diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index 793ed716..e864a778 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -15,10 +15,10 @@ import ( marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" 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/fifo" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" - "github.com/sirupsen/logrus" "gorm.io/gorm" ) @@ -30,12 +30,12 @@ type DeliveryOrdersService interface { } type deliveryOrdersService struct { - Log *logrus.Logger Validate *validator.Validate MarketingRepo marketingRepo.MarketingRepository MarketingProductRepo marketingRepo.MarketingProductRepository MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository ApprovalSvc commonSvc.ApprovalService + FifoSvc commonSvc.FifoService } func NewDeliveryOrdersService( @@ -43,15 +43,16 @@ func NewDeliveryOrdersService( marketingProductRepo marketingRepo.MarketingProductRepository, marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, + fifoSvc commonSvc.FifoService, validate *validator.Validate, ) DeliveryOrdersService { return &deliveryOrdersService{ - Log: utils.Log, Validate: validate, MarketingRepo: marketingRepo, MarketingProductRepo: marketingProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, ApprovalSvc: approvalSvc, + FifoSvc: fifoSvc, } } @@ -108,7 +109,6 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO }) if err != nil { - s.Log.Errorf("Failed to get marketings: %+v", err) return nil, 0, err } for i := range marketings { @@ -116,7 +116,7 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO return db.Preload("ActionUser") }) if err != nil { - s.Log.Warnf("Failed to load approval for marketing %d: %+v", marketings[i].Id, err) + continue } marketings[i].LatestApproval = latestApproval } @@ -247,7 +247,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery itemDeliveryDate = &parsedDate } - deliveryProduct.Qty = requestedProduct.Qty + deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.TotalWeight = requestedProduct.TotalWeight @@ -256,7 +256,8 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber 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 } } @@ -354,8 +355,9 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO itemDeliveryDate = deliveryProduct.DeliveryDate } - oldQty := deliveryProduct.Qty - deliveryProduct.Qty = requestedProduct.Qty + oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty + + deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.TotalWeight = requestedProduct.TotalWeight @@ -363,14 +365,18 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber - qtyChange := requestedProduct.Qty - oldQty - if qtyChange > 0 { - if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, qtyChange); err != nil { - return err + if requestedProduct.Qty != oldRequestedQty { + + if oldRequestedQty > 0 { + 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 { - return err + + if requestedProduct.Qty > 0 { + 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) } -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 { 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 errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found") + pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) + 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 { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for warehouse - available: %.2f, requested: %.2f", pw.Quantity, qtyDeliver)) + if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, result.PendingQuantity); err != nil { + 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 } -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 { return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") } - pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) - pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil) + deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx) + currentUsage, err := deliveryProductRepo.GetUsageQty(ctx, deliveryProduct.Id) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found") - } - - return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock") + currentUsage = 0 } - pw.Quantity = pw.Quantity + qtyRestore - if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock") + if currentUsage == 0 { + return nil + } + + 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 diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index 02cd2e42..bef2a477 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -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 errors.Is(err, gorm.ErrRecordNotFound) { + mdp := &entity.MarketingDeliveryProduct{ MarketingProductId: old.Id, - Qty: 0, UnitPrice: 0, TotalWeight: 0, AvgWeight: 0, TotalPrice: 0, DeliveryDate: nil, VehicleNumber: rp.VehicleNumber, + UsageQty: 0, + PendingQty: 0, } if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil { 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 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)) } @@ -602,13 +604,14 @@ func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Cont marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ MarketingProductId: marketingProduct.Id, - Qty: 0, UnitPrice: 0, TotalWeight: 0, AvgWeight: 0, TotalPrice: 0, DeliveryDate: nil, VehicleNumber: rp.VehicleNumber, + UsageQty: 0, + PendingQty: 0, } if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil { return err diff --git a/internal/modules/repports/dto/repportMarketing.dto.go b/internal/modules/repports/dto/repportMarketing.dto.go index 9c026590..90c2fe50 100644 --- a/internal/modules/repports/dto/repportMarketing.dto.go +++ b/internal/modules/repports/dto/repportMarketing.dto.go @@ -61,7 +61,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK 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 var hpp float64 @@ -78,7 +78,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK AgingDays: agingDays, DoNumber: doNumber, MarketingType: getMarketingType(mdp), - Qty: mdp.Qty, + Qty: mdp.UsageQty, AverageWeightKg: mdp.AvgWeight, TotalWeightKg: totalWeightKg, SalesPricePerKg: mdp.UnitPrice, @@ -194,8 +194,8 @@ func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, ca totalHppAmount := int64(0) for _, mdp := range mdps { - calculatedTotalWeight := mdp.Qty * mdp.AvgWeight - totalQty += int(mdp.Qty) + calculatedTotalWeight := mdp.UsageQty * mdp.AvgWeight + totalQty += int(mdp.UsageQty) totalWeightKg += calculatedTotalWeight totalSalesAmount += int64(calculatedTotalWeight * mdp.UnitPrice) diff --git a/internal/utils/fifo/constants.go b/internal/utils/fifo/constants.go index fd0bca06..ea6f96c0 100644 --- a/internal/utils/fifo/constants.go +++ b/internal/utils/fifo/constants.go @@ -1,6 +1,7 @@ package fifo const ( - UsableKeyRecordingStock UsableKey = "RECORDING_STOCK" - UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN" + UsableKeyRecordingStock UsableKey = "RECORDING_STOCK" + UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN" + UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY" )