mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-23 14:55:42 +00:00
Merge branch 'dev/teguh' into 'development'
FIX/BE: fix closing keuangan kalkulasi penjualan, marketing fifo, transfer laying fifo stock alocation See merge request mbugroup/lti-api!298
This commit is contained in:
@@ -41,6 +41,7 @@ type ProductionData struct {
|
|||||||
TotalWeightProduced float64
|
TotalWeightProduced float64
|
||||||
TotalEggWeightKg float64
|
TotalEggWeightKg float64
|
||||||
TotalWeightSold float64
|
TotalWeightSold float64
|
||||||
|
TotalBirdSold float64
|
||||||
TotalSalesAmount float64
|
TotalSalesAmount float64
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,6 +284,7 @@ func (s closingKeuanganService) calculateProductionData(c *fiber.Ctx, projectFlo
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
data.TotalWeightSold += delivery.TotalWeight
|
data.TotalWeightSold += delivery.TotalWeight
|
||||||
|
data.TotalBirdSold += delivery.UsageQty
|
||||||
data.TotalSalesAmount += delivery.TotalPrice
|
data.TotalSalesAmount += delivery.TotalPrice
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,46 +385,77 @@ func (s closingKeuanganService) buildHPPSection(c *fiber.Ctx, projectFlock *enti
|
|||||||
|
|
||||||
func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.ProjectFlock, costs *CostData, production *ProductionData) dto.ProfitLossSection {
|
func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.ProjectFlock, costs *CostData, production *ProductionData) dto.ProfitLossSection {
|
||||||
|
|
||||||
totalPopulationIn := production.TotalPopulationIn
|
|
||||||
totalWeightProduced := production.TotalWeightProduced
|
totalWeightProduced := production.TotalWeightProduced
|
||||||
totalEggWeightKg := production.TotalEggWeightKg
|
totalEggWeightKg := production.TotalEggWeightKg
|
||||||
totalSalesAmount := production.TotalSalesAmount
|
totalSalesAmount := production.TotalSalesAmount
|
||||||
totalWeightSold := production.TotalWeightSold
|
totalWeightSold := production.TotalWeightSold
|
||||||
|
totalBirdSold := production.TotalBirdSold
|
||||||
|
actualPopulation := production.TotalPopulationIn - production.TotalDepletion
|
||||||
|
|
||||||
weightForSales := totalWeightSold
|
isLaying := projectFlock.Category == string(utils.ProjectFlockCategoryLaying)
|
||||||
weightForCalculation := totalWeightProduced
|
|
||||||
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
|
||||||
weightForSales = totalWeightSold
|
|
||||||
weightForCalculation = totalEggWeightKg
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateProfitLossMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
|
// Fungsi untuk sales: LAYING = populasi aktual, GROWING = ekor terjual
|
||||||
if totalPopulationIn > 0 {
|
calculateSalesMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
|
||||||
rpPerBird = amount / totalPopulationIn
|
if isLaying {
|
||||||
}
|
if actualPopulation > 0 {
|
||||||
if weightForSales > 0 {
|
rpPerBird = amount / actualPopulation
|
||||||
rpPerKg = amount / weightForSales
|
}
|
||||||
|
if totalWeightSold > 0 {
|
||||||
|
rpPerKg = amount / totalWeightSold
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if totalBirdSold > 0 {
|
||||||
|
rpPerBird = amount / totalBirdSold
|
||||||
|
}
|
||||||
|
if totalWeightSold > 0 {
|
||||||
|
rpPerKg = amount / totalWeightSold
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
actualPopulation := production.TotalPopulationIn - production.TotalDepletion
|
// Fungsi untuk cost: per ekor = populasi aktual, per kg = LAYING telur produksi / GROWING ayam produksi
|
||||||
|
calculateCostMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
|
||||||
calculateMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
|
|
||||||
if actualPopulation > 0 {
|
if actualPopulation > 0 {
|
||||||
rpPerBird = amount / actualPopulation
|
rpPerBird = amount / actualPopulation
|
||||||
}
|
}
|
||||||
if weightForCalculation > 0 {
|
if isLaying {
|
||||||
rpPerKg = amount / weightForCalculation
|
if totalEggWeightKg > 0 {
|
||||||
|
rpPerKg = amount / totalEggWeightKg
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if totalWeightProduced > 0 {
|
||||||
|
rpPerKg = amount / totalWeightProduced
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fungsi untuk overhead/ekspedisi: LAYING = populasi aktual, GROWING = ekor terjual
|
||||||
|
calculateOverheadMetrics := func(amount float64) (rpPerBird, rpPerKg float64) {
|
||||||
|
if isLaying {
|
||||||
|
if actualPopulation > 0 {
|
||||||
|
rpPerBird = amount / actualPopulation
|
||||||
|
}
|
||||||
|
if totalWeightSold > 0 {
|
||||||
|
rpPerKg = amount / totalWeightSold
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if totalBirdSold > 0 {
|
||||||
|
rpPerBird = amount / totalBirdSold
|
||||||
|
}
|
||||||
|
if totalWeightSold > 0 {
|
||||||
|
rpPerKg = amount / totalWeightSold
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
plItems := []dto.ProfitLossItem{}
|
plItems := []dto.ProfitLossItem{}
|
||||||
|
|
||||||
salesRpPerBird, salesRpPerKg := calculateProfitLossMetrics(totalSalesAmount)
|
salesRpPerBird, salesRpPerKg := calculateSalesMetrics(totalSalesAmount)
|
||||||
salesLabel := "Penjualan Ayam"
|
salesLabel := "Penjualan Ayam"
|
||||||
if projectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
if isLaying {
|
||||||
salesLabel = "Penjualan Telur"
|
salesLabel = "Penjualan Telur"
|
||||||
}
|
}
|
||||||
plItems = append(plItems, dto.ToProfitLossItem(
|
plItems = append(plItems, dto.ToProfitLossItem(
|
||||||
@@ -435,23 +468,23 @@ func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.Proj
|
|||||||
))
|
))
|
||||||
|
|
||||||
totalSapronakAmount := costs.ChickenCost + costs.FeedCost + costs.OvkCost
|
totalSapronakAmount := costs.ChickenCost + costs.FeedCost + costs.OvkCost
|
||||||
_, sapronakRpPerKg := calculateMetrics(totalSapronakAmount)
|
|
||||||
sapronakRpPerBird := 0.0
|
sapronakRpPerBird := 0.0
|
||||||
|
sapronakRpPerKg := 0.0
|
||||||
for _, amount := range []float64{costs.ChickenCost, costs.FeedCost, costs.OvkCost} {
|
for _, amount := range []float64{costs.ChickenCost, costs.FeedCost, costs.OvkCost} {
|
||||||
rpPerBird, _ := calculateMetrics(amount)
|
rpPerBird, rpPerKg := calculateCostMetrics(amount)
|
||||||
sapronakRpPerBird += rpPerBird
|
sapronakRpPerBird += rpPerBird
|
||||||
|
sapronakRpPerKg += rpPerKg
|
||||||
}
|
}
|
||||||
sapronakLabel := "Pengeluaran Sapronak"
|
|
||||||
plItems = append(plItems, dto.ToProfitLossItem(
|
plItems = append(plItems, dto.ToProfitLossItem(
|
||||||
string(dto.PLCodeSapronak),
|
string(dto.PLCodeSapronak),
|
||||||
sapronakLabel,
|
"Pengeluaran Sapronak",
|
||||||
"purchase",
|
"purchase",
|
||||||
sapronakRpPerBird,
|
sapronakRpPerBird,
|
||||||
sapronakRpPerKg,
|
sapronakRpPerKg,
|
||||||
totalSapronakAmount,
|
totalSapronakAmount,
|
||||||
))
|
))
|
||||||
|
|
||||||
overheadRpPerBird, overheadRpPerKg := calculateProfitLossMetrics(costs.RealizationOperational)
|
overheadRpPerBird, overheadRpPerKg := calculateOverheadMetrics(costs.RealizationOperational)
|
||||||
plItems = append(plItems, dto.ToProfitLossItem(
|
plItems = append(plItems, dto.ToProfitLossItem(
|
||||||
string(dto.PLCodeOverhead),
|
string(dto.PLCodeOverhead),
|
||||||
"Overhead",
|
"Overhead",
|
||||||
@@ -461,7 +494,7 @@ func (s closingKeuanganService) buildProfitLossSection(projectFlock *entity.Proj
|
|||||||
costs.RealizationOperational,
|
costs.RealizationOperational,
|
||||||
))
|
))
|
||||||
|
|
||||||
ekspedisiRpPerBird, ekspedisiRpPerKg := calculateProfitLossMetrics(costs.ExpeditionCost)
|
ekspedisiRpPerBird, ekspedisiRpPerKg := calculateOverheadMetrics(costs.ExpeditionCost)
|
||||||
plItems = append(plItems, dto.ToProfitLossItem(
|
plItems = append(plItems, dto.ToProfitLossItem(
|
||||||
string(dto.PLCodeEkspedisi),
|
string(dto.PLCodeEkspedisi),
|
||||||
"Ekspedisi",
|
"Ekspedisi",
|
||||||
|
|||||||
@@ -160,7 +160,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||||
}
|
}
|
||||||
|
|
||||||
afterQuantity := productWarehouse.Quantity
|
|
||||||
newLog := &entity.StockLog{
|
newLog := &entity.StockLog{
|
||||||
LoggableType: string(utils.StockLogTypeAdjustment),
|
LoggableType: string(utils.StockLogTypeAdjustment),
|
||||||
LoggableId: 0,
|
LoggableId: 0,
|
||||||
@@ -183,14 +182,12 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
||||||
afterQuantity += req.Quantity
|
|
||||||
newLog.Increase = req.Quantity
|
newLog.Increase = req.Quantity
|
||||||
newLog.Stock += newLog.Increase
|
newLog.Stock += newLog.Increase
|
||||||
} else {
|
} else {
|
||||||
if productWarehouse.Quantity < req.Quantity {
|
if productWarehouse.Quantity < req.Quantity {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk pengurangan. Stok saat ini: %.2f, Jumlah yang akan dikurangi: %.2f", productWarehouse.Quantity, req.Quantity))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak mencukupi untuk pengurangan. Stok saat ini: %.2f, Jumlah yang akan dikurangi: %.2f", productWarehouse.Quantity, req.Quantity))
|
||||||
}
|
}
|
||||||
afterQuantity -= req.Quantity
|
|
||||||
newLog.Decrease = req.Quantity
|
newLog.Decrease = req.Quantity
|
||||||
newLog.Stock -= newLog.Decrease
|
newLog.Stock -= newLog.Decrease
|
||||||
}
|
}
|
||||||
@@ -243,12 +240,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
productWarehouse.Quantity = afterQuantity
|
|
||||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil {
|
|
||||||
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
createdAdjustmentStockId = adjustmentStock.Id
|
createdAdjustmentStockId = adjustmentStock.Id
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ 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, warehouseRepo, projectFlockKandangRepo, validate)
|
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, fifoService, warehouseRepo, projectFlockKandangRepo, validate)
|
||||||
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoService, validate)
|
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, stockLogRepo, approvalSvc, fifoService, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
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"
|
||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
|
||||||
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"
|
||||||
@@ -502,78 +501,33 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor
|
|||||||
Tx: tx,
|
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)
|
||||||
|
|
||||||
totalConsumed := 0.0
|
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, 0); err != nil {
|
||||||
var fifoConsumed float64
|
|
||||||
var directConsumed float64
|
|
||||||
|
|
||||||
if result != nil && result.UsageQuantity > 0 {
|
|
||||||
fifoConsumed = result.UsageQuantity
|
|
||||||
totalConsumed = result.UsageQuantity
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil || (totalConsumed < requestedQty) {
|
|
||||||
remainder := requestedQty - totalConsumed
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
if pw == nil || pw.Quantity < remainder {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. FIFO: %.2f, Direct Available: %.2f, Total Needed: %.2f", func() float64 {
|
|
||||||
if pw != nil {
|
|
||||||
return pw.Quantity
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}(), remainder, requestedQty))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := pwRepo.AdjustQuantities(ctx, map[uint]float64{
|
|
||||||
marketingProduct.ProductWarehouseId: -remainder,
|
|
||||||
}, func(db *gorm.DB) *gorm.DB {
|
|
||||||
return tx
|
|
||||||
}); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to adjust product warehouse quantity")
|
|
||||||
}
|
|
||||||
|
|
||||||
directConsumed = remainder
|
|
||||||
totalConsumed += remainder
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, totalConsumed, 0); 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 && totalConsumed > 0 {
|
if actorID > 0 && result.UsageQuantity > 0 {
|
||||||
notes := ""
|
|
||||||
if fifoConsumed > 0 && directConsumed > 0 {
|
|
||||||
notes = fmt.Sprintf("Partial FIFO (%.2f) + Direct (%.2f)", fifoConsumed, directConsumed)
|
|
||||||
} else if fifoConsumed > 0 {
|
|
||||||
notes = fmt.Sprintf("FIFO stock only (%.2f)", fifoConsumed)
|
|
||||||
} else if directConsumed > 0 {
|
|
||||||
notes = fmt.Sprintf("Direct stock only (%.2f)", directConsumed)
|
|
||||||
}
|
|
||||||
|
|
||||||
decreaseLog := &entity.StockLog{
|
decreaseLog := &entity.StockLog{
|
||||||
Decrease: totalConsumed,
|
Decrease: result.UsageQuantity,
|
||||||
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: notes,
|
Notes: fmt.Sprintf("FIFO consume (%.2f)", result.UsageQuantity),
|
||||||
}
|
}
|
||||||
|
|
||||||
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
|
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.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")
|
||||||
}
|
}
|
||||||
if len(stockLogs) > 0 {
|
if len(stockLogs) > 0 {
|
||||||
latestStockLog := stockLogs[0]
|
latestStockLog := stockLogs[0]
|
||||||
decreaseLog.Stock = latestStockLog.Stock
|
decreaseLog.Stock = latestStockLog.Stock - decreaseLog.Decrease
|
||||||
decreaseLog.Stock -= decreaseLog.Decrease
|
|
||||||
} else {
|
} else {
|
||||||
decreaseLog.Stock -= decreaseLog.Decrease
|
decreaseLog.Stock -= decreaseLog.Decrease
|
||||||
}
|
}
|
||||||
@@ -610,6 +564,10 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if actorID > 0 && currentUsage > 0 {
|
if actorID > 0 && currentUsage > 0 {
|
||||||
increaseLog := &entity.StockLog{
|
increaseLog := &entity.StockLog{
|
||||||
Increase: currentUsage,
|
Increase: currentUsage,
|
||||||
@@ -617,7 +575,7 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
|
|||||||
LoggableId: deliveryProduct.Id,
|
LoggableId: deliveryProduct.Id,
|
||||||
ProductWarehouseId: marketingProduct.ProductWarehouseId,
|
ProductWarehouseId: marketingProduct.ProductWarehouseId,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
Notes: "",
|
Notes: fmt.Sprintf("Release delivery stock (%.2f)", currentUsage),
|
||||||
}
|
}
|
||||||
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
|
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -625,8 +583,7 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
|
|||||||
}
|
}
|
||||||
if len(stockLogs) > 0 {
|
if len(stockLogs) > 0 {
|
||||||
latestStockLog := stockLogs[0]
|
latestStockLog := stockLogs[0]
|
||||||
increaseLog.Stock = latestStockLog.Stock
|
increaseLog.Stock = latestStockLog.Stock + increaseLog.Increase
|
||||||
increaseLog.Stock += increaseLog.Increase
|
|
||||||
} else {
|
} else {
|
||||||
increaseLog.Stock += increaseLog.Increase
|
increaseLog.Stock += increaseLog.Increase
|
||||||
}
|
}
|
||||||
@@ -634,9 +591,5 @@ func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gor
|
|||||||
s.StockLogRepo.WithTx(tx).CreateOne(ctx, increaseLog, nil)
|
s.StockLogRepo.WithTx(tx).CreateOne(ctx, increaseLog, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ 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"
|
||||||
@@ -41,11 +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
|
||||||
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, warehouseRepo warehouseRepo.WarehouseRepository,
|
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, fifoSvc commonSvc.FifoService, 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,
|
||||||
@@ -55,6 +57,7 @@ func NewSalesOrdersService(marketingRepo repository.MarketingRepository, custome
|
|||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
UserRepo: userRepo,
|
UserRepo: userRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
|
FifoSvc: fifoSvc,
|
||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
}
|
}
|
||||||
@@ -230,14 +233,14 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(req.MarketingProducts) > 0 {
|
if len(req.MarketingProducts) > 0 {
|
||||||
for _, item := range req.MarketingProducts {
|
for _, item := range req.MarketingProducts {
|
||||||
if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil {
|
if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := commonSvc.EnsureRelations(c.Context(),
|
if err := commonSvc.EnsureRelations(c.Context(),
|
||||||
commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists},
|
commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,6 +336,32 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
|||||||
totalPrice = totalWeight * rp.UnitPrice
|
totalPrice = totalWeight * rp.UnitPrice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id)
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil && deliveryProduct.Id != 0 {
|
||||||
|
oldQty := old.Qty
|
||||||
|
newQty := rp.Qty
|
||||||
|
qtyDiff := newQty - oldQty
|
||||||
|
|
||||||
|
if qtyDiff < 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Cannot decrease quantity after stock has been allocated. Please delete and create new product.")
|
||||||
|
} else if qtyDiff > 0 {
|
||||||
|
_, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
|
||||||
|
UsableKey: fifo.UsableKeyMarketingDelivery,
|
||||||
|
UsableID: deliveryProduct.Id,
|
||||||
|
ProductWarehouseID: rp.ProductWarehouseId,
|
||||||
|
Quantity: qtyDiff,
|
||||||
|
Tx: dbTransaction,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Insufficient stock for additional quantity: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateBody := map[string]any{
|
updateBody := map[string]any{
|
||||||
"product_warehouse_id": rp.ProductWarehouseId,
|
"product_warehouse_id": rp.ProductWarehouseId,
|
||||||
"qty": rp.Qty,
|
"qty": rp.Qty,
|
||||||
@@ -345,25 +374,20 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil {
|
if deliveryProduct.Id == 0 {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
mdp := &entity.MarketingDeliveryProduct{
|
||||||
|
MarketingProductId: old.Id,
|
||||||
mdp := &entity.MarketingDeliveryProduct{
|
UnitPrice: 0,
|
||||||
MarketingProductId: old.Id,
|
TotalWeight: 0,
|
||||||
UnitPrice: 0,
|
AvgWeight: 0,
|
||||||
TotalWeight: 0,
|
TotalPrice: 0,
|
||||||
AvgWeight: 0,
|
DeliveryDate: nil,
|
||||||
TotalPrice: 0,
|
VehicleNumber: rp.VehicleNumber,
|
||||||
DeliveryDate: nil,
|
UsageQty: 0,
|
||||||
VehicleNumber: rp.VehicleNumber,
|
PendingQty: 0,
|
||||||
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")
|
||||||
if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -380,10 +404,18 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
|||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product")
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil && deliveryProduct.Id != 0 {
|
||||||
|
|
||||||
if deliveryProduct.DeliveryDate != nil || deliveryProduct.UsageQty > 0 || deliveryProduct.PendingQty > 0 {
|
if deliveryProduct.DeliveryDate != nil {
|
||||||
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 been delivered", old.Id))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{
|
||||||
|
UsableKey: fifo.UsableKeyMarketingDelivery,
|
||||||
|
UsableID: deliveryProduct.Id,
|
||||||
|
Tx: dbTransaction,
|
||||||
|
}); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil {
|
if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil {
|
||||||
@@ -459,6 +491,19 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
marketingRepoTx := repository.NewMarketingRepository(dbTransaction)
|
||||||
|
|
||||||
if len(marketing.Products) > 0 {
|
if len(marketing.Products) > 0 {
|
||||||
|
deliveryProducts, err := marketingDeliveryProductRepoTx.GetByMarketingId(c.Context(), marketing.Id)
|
||||||
|
if err == nil && len(deliveryProducts) > 0 {
|
||||||
|
for _, dp := range deliveryProducts {
|
||||||
|
if err := s.FifoSvc.ReleaseUsage(c.Context(), commonSvc.StockReleaseRequest{
|
||||||
|
UsableKey: fifo.UsableKeyMarketingDelivery,
|
||||||
|
UsableID: dp.Id,
|
||||||
|
Tx: dbTransaction,
|
||||||
|
}); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for delivery product %d: %v", dp.Id, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, product := range marketing.Products {
|
for _, product := range marketing.Products {
|
||||||
if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB {
|
if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB {
|
||||||
return db.Where("marketing_product_id = ?", product.Id).Unscoped()
|
return db.Where("marketing_product_id = ?", product.Id).Unscoped()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -743,7 +744,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
|||||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
repoTx := s.Repository.WithTx(dbTransaction)
|
repoTx := s.Repository.WithTx(dbTransaction)
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
stockAllocationRepo := commonRepo.NewStockAllocationRepository(dbTransaction)
|
||||||
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
||||||
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
|
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
|
||||||
stockLogRepoTx := rStockLogs.NewStockLogRepository(dbTransaction)
|
stockLogRepoTx := rStockLogs.NewStockLogRepository(dbTransaction)
|
||||||
@@ -817,6 +818,27 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetShares := distributeProportionalWithRounding(targets, totalTargetQty, sourceShare)
|
||||||
|
|
||||||
|
for i, target := range targets {
|
||||||
|
roundedQty := math.Round(targetShares[i])
|
||||||
|
if roundedQty <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mappingAllocation := &entity.StockAllocation{
|
||||||
|
StockableType: fifo.UsableKeyTransferToLayingOut.String(),
|
||||||
|
StockableId: source.Id,
|
||||||
|
UsableType: fifo.StockableKeyTransferToLayingIn.String(),
|
||||||
|
UsableId: target.Id,
|
||||||
|
ProductWarehouseId: *source.ProductWarehouseId,
|
||||||
|
Qty: roundedQty,
|
||||||
|
Status: entity.StockAllocationStatusActive,
|
||||||
|
}
|
||||||
|
if err := stockAllocationRepo.CreateOne(c.Context(), mappingAllocation, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal create mapping allocation source→target")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stockLogDecrease := &entity.StockLog{
|
stockLogDecrease := &entity.StockLog{
|
||||||
ProductWarehouseId: *source.ProductWarehouseId,
|
ProductWarehouseId: *source.ProductWarehouseId,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
@@ -937,36 +959,6 @@ func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, tx *gorm.DB, productID uint, warehouseID uint, quantity float64, actorID uint, projectFlockKandangId *uint) (*entity.ProductWarehouse, error) {
|
|
||||||
|
|
||||||
productWarehouseRepoTx := rInventory.NewProductWarehouseRepository(tx)
|
|
||||||
|
|
||||||
existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
|
|
||||||
if err == nil && existing != nil {
|
|
||||||
|
|
||||||
if err := productWarehouseRepoTx.PatchOne(ctx, existing.Id, map[string]any{"qty": gorm.Expr("qty + ?", quantity)}, nil); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return existing, nil
|
|
||||||
}
|
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
newWarehouse := &entity.ProductWarehouse{
|
|
||||||
ProductId: productID,
|
|
||||||
WarehouseId: warehouseID,
|
|
||||||
ProjectFlockKandangId: projectFlockKandangId,
|
|
||||||
Quantity: quantity,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return newWarehouse, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) {
|
func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) {
|
||||||
|
|
||||||
pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
|
pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB {
|
||||||
@@ -1060,3 +1052,34 @@ func (s transferLayingService) GetMaxTargetQtyPerKandang(c *fiber.Ctx, projectFl
|
|||||||
|
|
||||||
return kandangMaxTargetQty, nil
|
return kandangMaxTargetQty, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func distributeProportionalWithRounding(targets []entity.LayingTransferTarget, totalTargetQty, sourceShare float64) []float64 {
|
||||||
|
if len(targets) == 0 {
|
||||||
|
return []float64{}
|
||||||
|
}
|
||||||
|
|
||||||
|
targetShares := make([]float64, len(targets))
|
||||||
|
totalRounded := 0.0
|
||||||
|
|
||||||
|
for i, target := range targets {
|
||||||
|
targetShares[i] = (target.TotalQty / totalTargetQty) * sourceShare
|
||||||
|
totalRounded += math.Round(targetShares[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
diff := sourceShare - totalRounded
|
||||||
|
|
||||||
|
if diff != 0 {
|
||||||
|
maxIdx := 0
|
||||||
|
maxDecimal := 0.0
|
||||||
|
for i, share := range targetShares {
|
||||||
|
decimal := share - math.Round(share)
|
||||||
|
if decimal > maxDecimal {
|
||||||
|
maxDecimal = decimal
|
||||||
|
maxIdx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
targetShares[maxIdx] += diff
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetShares
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user