mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
fixing filter pw for transfer, add transfer delete
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
package entities
|
package entities
|
||||||
|
|
||||||
type ProductWarehouse struct {
|
type ProductWarehouse struct {
|
||||||
Id uint `gorm:"primaryKey;column:id"`
|
Id uint `gorm:"primaryKey;column:id"`
|
||||||
ProductId uint `gorm:"column:product_id;not null"`
|
ProductId uint `gorm:"column:product_id;not null"`
|
||||||
WarehouseId uint `gorm:"column:warehouse_id;not null"`
|
WarehouseId uint `gorm:"column:warehouse_id;not null"`
|
||||||
ProjectFlockKandangId *uint `gorm:"column:project_flock_kandang_id"`
|
ProjectFlockKandangId *uint `gorm:"column:project_flock_kandang_id"`
|
||||||
Quantity float64 `gorm:"column:qty;type:numeric(15,3);default:0"`
|
Quantity float64 `gorm:"column:qty;type:numeric(15,3);default:0"`
|
||||||
|
AvailableQty *float64 `gorm:"-"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Product Product `gorm:"foreignKey:ProductId;references:Id"`
|
Product Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const (
|
|||||||
P_TransferGetAll = "lti.inventory.transfer.list"
|
P_TransferGetAll = "lti.inventory.transfer.list"
|
||||||
P_TransferGetOne = "lti.inventory.transfer.detail"
|
P_TransferGetOne = "lti.inventory.transfer.detail"
|
||||||
P_TransferCreateOne = "lti.inventory.transfer.create"
|
P_TransferCreateOne = "lti.inventory.transfer.create"
|
||||||
|
P_TransferDeleteOne = "lti.inventory.transfer.delete"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
+1
@@ -32,6 +32,7 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error {
|
|||||||
Flags: c.Query("flags", ""),
|
Flags: c.Query("flags", ""),
|
||||||
KandangId: uint(c.QueryInt("kandang_id", 0)),
|
KandangId: uint(c.QueryInt("kandang_id", 0)),
|
||||||
TransferContext: c.Query(utils.TransferContextKey, ""),
|
TransferContext: c.Query(utils.TransferContextKey, ""),
|
||||||
|
StockMode: c.Query("stock_mode", ""),
|
||||||
Type: c.Query("type", ""),
|
Type: c.Query("type", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import (
|
|||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
|
|
||||||
type ProductWarehouseRelationDTO struct {
|
type ProductWarehouseRelationDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
ProductId uint `json:"product_id"`
|
ProductId uint `json:"product_id"`
|
||||||
WarehouseId uint `json:"warehouse_id"`
|
WarehouseId uint `json:"warehouse_id"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
|
AvailableQty *float64 `json:"available_qty,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductWarehouseListDTO struct {
|
type ProductWarehouseListDTO struct {
|
||||||
@@ -61,10 +62,11 @@ type ProjectFlockRelationDTO struct {
|
|||||||
|
|
||||||
func ToProductWarehouseRelationDTO(e entity.ProductWarehouse) ProductWarehouseRelationDTO {
|
func ToProductWarehouseRelationDTO(e entity.ProductWarehouse) ProductWarehouseRelationDTO {
|
||||||
return ProductWarehouseRelationDTO{
|
return ProductWarehouseRelationDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
ProductId: e.ProductId, // Field yang benar dari entity
|
ProductId: e.ProductId, // Field yang benar dari entity
|
||||||
WarehouseId: e.WarehouseId, // Field yang benar dari entity
|
WarehouseId: e.WarehouseId, // Field yang benar dari entity
|
||||||
Quantity: e.Quantity,
|
Quantity: e.Quantity,
|
||||||
|
AvailableQty: e.AvailableQty,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations"
|
||||||
kandangrepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
kandangrepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +29,8 @@ type productWarehouseService struct {
|
|||||||
KandangRepo kandangrepo.KandangRepository
|
KandangRepo kandangrepo.KandangRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stockModeExcludeChickin = "exclude_chickin"
|
||||||
|
|
||||||
func NewProductWarehouseService(repo repository.ProductWarehouseRepository, validate *validator.Validate, kandangRepo kandangrepo.KandangRepository) ProductWarehouseService {
|
func NewProductWarehouseService(repo repository.ProductWarehouseRepository, validate *validator.Validate, kandangRepo kandangrepo.KandangRepository) ProductWarehouseService {
|
||||||
return &productWarehouseService{
|
return &productWarehouseService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
@@ -189,6 +193,11 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
|||||||
s.Log.Errorf("Failed to get productWarehouses: %+v", err)
|
s.Log.Errorf("Failed to get productWarehouses: %+v", err)
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
productWarehouses, err = s.applyTransferAvailableQty(c, params, productWarehouses)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
return productWarehouses, total, nil
|
return productWarehouses, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,3 +238,80 @@ func (s productWarehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductW
|
|||||||
}
|
}
|
||||||
return productWarehouse, nil
|
return productWarehouse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s productWarehouseService) applyTransferAvailableQty(c *fiber.Ctx, params *validation.Query, rows []entity.ProductWarehouse) ([]entity.ProductWarehouse, error) {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
if params == nil ||
|
||||||
|
params.TransferContext != utils.TransferContextInventoryTransfer ||
|
||||||
|
params.StockMode != stockModeExcludeChickin {
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ayamPWIDs := make([]uint, 0)
|
||||||
|
for i := range rows {
|
||||||
|
if isAyamProductByFlags(rows[i].Product.Flags) {
|
||||||
|
ayamPWIDs = append(ayamPWIDs, rows[i].Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(ayamPWIDs) == 0 {
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type usageRow struct {
|
||||||
|
ProductWarehouseID uint `gorm:"column:product_warehouse_id"`
|
||||||
|
UsedQty float64 `gorm:"column:used_qty"`
|
||||||
|
}
|
||||||
|
usageRows := make([]usageRow, 0)
|
||||||
|
if err := s.Repository.DB().WithContext(c.Context()).
|
||||||
|
Table("stock_allocations").
|
||||||
|
Select("product_warehouse_id, COALESCE(SUM(qty), 0) AS used_qty").
|
||||||
|
Where("product_warehouse_id IN ?", ayamPWIDs).
|
||||||
|
Where("usable_type = ?", fifo.UsableKeyProjectChickin.String()).
|
||||||
|
Where("status = ?", entity.StockAllocationStatusActive).
|
||||||
|
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Group("product_warehouse_id").
|
||||||
|
Scan(&usageRows).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to calculate available transfer stock after chickin consumption: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal menghitung stok tersedia untuk transfer")
|
||||||
|
}
|
||||||
|
|
||||||
|
usageMap := make(map[uint]float64, len(usageRows))
|
||||||
|
for _, row := range usageRows {
|
||||||
|
usageMap[row.ProductWarehouseID] = row.UsedQty
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := make([]entity.ProductWarehouse, 0, len(rows))
|
||||||
|
for i := range rows {
|
||||||
|
row := rows[i]
|
||||||
|
if !isAyamProductByFlags(row.Product.Flags) {
|
||||||
|
filtered = append(filtered, row)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
available := row.Quantity - usageMap[row.Id]
|
||||||
|
if available < 0 {
|
||||||
|
available = 0
|
||||||
|
}
|
||||||
|
row.AvailableQty = &available
|
||||||
|
|
||||||
|
if available <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = append(filtered, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAyamProductByFlags(flags []entity.Flag) bool {
|
||||||
|
for _, flag := range flags {
|
||||||
|
if utils.CanonicalFlagType(strings.TrimSpace(flag.Name)) == utils.FlagAyam {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
+1
@@ -20,5 +20,6 @@ type Query struct {
|
|||||||
Flags string `query:"flags" validate:"omitempty"`
|
Flags string `query:"flags" validate:"omitempty"`
|
||||||
KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"`
|
KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"`
|
||||||
TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"`
|
TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"`
|
||||||
|
StockMode string `query:"stock_mode" validate:"omitempty,oneof=exclude_chickin"`
|
||||||
Type string `query:"type" validate:"omitempty"`
|
Type string `query:"type" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,3 +109,23 @@ func (u *TransferController) CreateOne(c *fiber.Ctx) error {
|
|||||||
Data: dto.ToTransferDetailDTO(*result),
|
Data: dto.ToTransferDetailDTO(*result),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *TransferController) DeleteOne(c *fiber.Ctx) error {
|
||||||
|
param := c.Params("id")
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(param)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := u.TransferService.DeleteOne(c, uint(id)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Common{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Delete transfer successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,5 +18,6 @@ func TransferRoutes(v1 fiber.Router, u user.UserService, s transfer.TransferServ
|
|||||||
route.Get("/", m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
|
route.Get("/", m.RequirePermissions(m.P_TransferGetAll), ctrl.GetAll)
|
||||||
route.Post("/", m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
|
route.Post("/", m.RequirePermissions(m.P_TransferCreateOne), ctrl.CreateOne)
|
||||||
route.Get("/:id", m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne)
|
route.Get("/:id", m.RequirePermissions(m.P_TransferGetOne), ctrl.GetOne)
|
||||||
|
route.Delete("/:id", m.RequirePermissions(m.P_TransferDeleteOne), ctrl.DeleteOne)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -21,14 +23,17 @@ import (
|
|||||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TransferService interface {
|
type TransferService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.StockTransfer, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.StockTransfer, error)
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error)
|
||||||
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type transferService struct {
|
type transferService struct {
|
||||||
@@ -49,6 +54,8 @@ type transferService struct {
|
|||||||
ExpenseBridge TransferExpenseBridge
|
ExpenseBridge TransferExpenseBridge
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const transferDeleteDownstreamGuardMessage = "Transfer stock tidak dapat dihapus karena stok transfer sudah dipakai transaksi turunan. Hapus dependensi terkait secara manual terlebih dahulu."
|
||||||
|
|
||||||
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, projectFlockPopulationRepo projectFlockKandangRepo.ProjectFlockPopulationRepository, documentSvc commonSvc.DocumentService, fifoStockV2Svc commonSvc.FifoStockV2Service, expenseBridge TransferExpenseBridge) TransferService {
|
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, projectFlockPopulationRepo projectFlockKandangRepo.ProjectFlockPopulationRepository, documentSvc commonSvc.DocumentService, fifoStockV2Svc commonSvc.FifoStockV2Service, expenseBridge TransferExpenseBridge) TransferService {
|
||||||
return &transferService{
|
return &transferService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
@@ -104,6 +111,7 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
|||||||
|
|
||||||
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
transfers, total, err := s.StockTransferRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
|
db = db.Where("stock_transfers.deleted_at IS NULL")
|
||||||
if scope.Restrict {
|
if scope.Restrict {
|
||||||
if len(scope.IDs) == 0 {
|
if len(scope.IDs) == 0 {
|
||||||
return db.Where("1 = 0")
|
return db.Where("1 = 0")
|
||||||
@@ -145,6 +153,7 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
|
|||||||
Joins("JOIN warehouses w_from ON w_from.id = stock_transfers.from_warehouse_id").
|
Joins("JOIN warehouses w_from ON w_from.id = stock_transfers.from_warehouse_id").
|
||||||
Joins("JOIN warehouses w_to ON w_to.id = stock_transfers.to_warehouse_id").
|
Joins("JOIN warehouses w_to ON w_to.id = stock_transfers.to_warehouse_id").
|
||||||
Where("stock_transfers.id = ?", id).
|
Where("stock_transfers.id = ?", id).
|
||||||
|
Where("stock_transfers.deleted_at IS NULL").
|
||||||
Where("w_from.location_id IN ? OR w_to.location_id IN ?", scope.IDs, scope.IDs).
|
Where("w_from.location_id IN ? OR w_to.location_id IN ?", scope.IDs, scope.IDs).
|
||||||
Count(&count).Error; err != nil {
|
Count(&count).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -155,7 +164,7 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
return s.withRelations(db)
|
return s.withRelations(db).Where("stock_transfers.deleted_at IS NULL")
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -619,6 +628,210 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *transferService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
|
if err := s.ensureTransferAccess(c.Context(), id, c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s.FifoStockV2Svc == nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var deletedDetails []entity.StockTransferDetail
|
||||||
|
err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||||
|
stockLogRepoTx := rStockLogs.NewStockLogRepository(tx)
|
||||||
|
|
||||||
|
var transfer entity.StockTransfer
|
||||||
|
if err := tx.WithContext(c.Context()).
|
||||||
|
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
|
Where("id = ?", uint64(id)).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Take(&transfer).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Transfer dengan ID %d tidak ditemukan", id))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data transfer")
|
||||||
|
}
|
||||||
|
|
||||||
|
var details []entity.StockTransferDetail
|
||||||
|
if err := tx.WithContext(c.Context()).
|
||||||
|
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
|
Where("stock_transfer_id = ?", transfer.Id).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Order("id ASC").
|
||||||
|
Find(&details).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil detail transfer")
|
||||||
|
}
|
||||||
|
if len(details) == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Transfer tidak memiliki detail produk")
|
||||||
|
}
|
||||||
|
|
||||||
|
detailIDs := make([]uint64, 0, len(details))
|
||||||
|
for _, detail := range details {
|
||||||
|
detailIDs = append(detailIDs, detail.Id)
|
||||||
|
}
|
||||||
|
if err := s.ensureNoDownstreamConsumptionForDelete(c.Context(), tx, detailIDs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type reflowKey struct {
|
||||||
|
flagGroupCode string
|
||||||
|
productWarehouseID uint
|
||||||
|
}
|
||||||
|
destReflows := make(map[reflowKey]struct{})
|
||||||
|
|
||||||
|
for _, detail := range details {
|
||||||
|
if detail.SourceProductWarehouseID == nil || *detail.SourceProductWarehouseID == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Detail transfer %d tidak memiliki source product warehouse valid", detail.Id))
|
||||||
|
}
|
||||||
|
if detail.DestProductWarehouseID == nil || *detail.DestProductWarehouseID == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Detail transfer %d tidak memiliki destination product warehouse valid", detail.Id))
|
||||||
|
}
|
||||||
|
|
||||||
|
flagGroupCode, err := s.resolveTransferFlagGroup(c.Context(), tx, uint(detail.ProductId))
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("FIFO v2 route tidak ditemukan untuk produk %d: %v", detail.ProductId, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
rollbackRes, err := s.FifoStockV2Svc.Rollback(c.Context(), commonSvc.FifoStockV2RollbackRequest{
|
||||||
|
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
|
||||||
|
Usable: commonSvc.FifoStockV2Ref{
|
||||||
|
ID: uint(detail.Id),
|
||||||
|
LegacyTypeKey: fifo.UsableKeyStockTransferOut.String(),
|
||||||
|
FunctionCode: "STOCK_TRANSFER_OUT",
|
||||||
|
},
|
||||||
|
Reason: fmt.Sprintf("transfer delete #%s", transfer.MovementNumber),
|
||||||
|
Tx: tx,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback FIFO v2 transfer detail %d: %v", detail.Id, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
releasedQty := 0.0
|
||||||
|
if rollbackRes != nil {
|
||||||
|
releasedQty = rollbackRes.ReleasedQty
|
||||||
|
}
|
||||||
|
if detail.UsageQty > 1e-6 && releasedQty < detail.UsageQty-1e-6 {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Rollback FIFO v2 source transfer detail %d tidak lengkap. Dibutuhkan %.3f, terlepas %.3f", detail.Id, detail.UsageQty, releasedQty),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if releasedQty > 1e-6 {
|
||||||
|
if err := s.appendStockLog(
|
||||||
|
c.Context(),
|
||||||
|
stockLogRepoTx,
|
||||||
|
uint(*detail.SourceProductWarehouseID),
|
||||||
|
actorID,
|
||||||
|
releasedQty,
|
||||||
|
0,
|
||||||
|
uint(detail.Id),
|
||||||
|
fmt.Sprintf("TRANSFER DELETE #%s", transfer.MovementNumber),
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destDecreaseQty := detail.TotalQty
|
||||||
|
if destDecreaseQty <= 1e-6 {
|
||||||
|
destDecreaseQty = detail.UsageQty
|
||||||
|
}
|
||||||
|
if destDecreaseQty > 1e-6 {
|
||||||
|
if err := s.appendStockLog(
|
||||||
|
c.Context(),
|
||||||
|
stockLogRepoTx,
|
||||||
|
uint(*detail.DestProductWarehouseID),
|
||||||
|
actorID,
|
||||||
|
0,
|
||||||
|
destDecreaseQty,
|
||||||
|
uint(detail.Id),
|
||||||
|
fmt.Sprintf("TRANSFER DELETE #%s", transfer.MovementNumber),
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destReflows[reflowKey{
|
||||||
|
flagGroupCode: flagGroupCode,
|
||||||
|
productWarehouseID: uint(*detail.DestProductWarehouseID),
|
||||||
|
}] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
if err := tx.WithContext(c.Context()).
|
||||||
|
Where("stock_transfer_detail_id IN ?", detailIDs).
|
||||||
|
Delete(&entity.StockTransferDeliveryItem{}).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal menghapus item delivery transfer")
|
||||||
|
}
|
||||||
|
if err := tx.WithContext(c.Context()).
|
||||||
|
Model(&entity.StockTransferDelivery{}).
|
||||||
|
Where("stock_transfer_id = ?", transfer.Id).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Updates(map[string]any{
|
||||||
|
"deleted_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal menghapus delivery transfer")
|
||||||
|
}
|
||||||
|
if err := tx.WithContext(c.Context()).
|
||||||
|
Model(&entity.StockTransferDetail{}).
|
||||||
|
Where("id IN ?", detailIDs).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Updates(map[string]any{
|
||||||
|
"deleted_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal menghapus detail transfer")
|
||||||
|
}
|
||||||
|
|
||||||
|
asOf := transfer.TransferDate
|
||||||
|
for key := range destReflows {
|
||||||
|
if _, err := s.FifoStockV2Svc.Reflow(c.Context(), commonSvc.FifoStockV2ReflowRequest{
|
||||||
|
FlagGroupCode: key.flagGroupCode,
|
||||||
|
ProductWarehouseID: key.productWarehouseID,
|
||||||
|
AsOf: &asOf,
|
||||||
|
Tx: tx,
|
||||||
|
}); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal reflow stok tujuan saat delete transfer: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.WithContext(c.Context()).
|
||||||
|
Model(&entity.StockTransfer{}).
|
||||||
|
Where("id = ?", transfer.Id).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Updates(map[string]any{
|
||||||
|
"deleted_at": now,
|
||||||
|
"updated_at": now,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal menghapus transfer")
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedDetails = append(deletedDetails, details...)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return fiberErr
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal menghapus transfer")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(deletedDetails) > 0 && s.ExpenseBridge != nil {
|
||||||
|
if err := s.ExpenseBridge.OnItemsDeleted(c.Context(), uint64(id), deletedDetails); err != nil {
|
||||||
|
s.Log.Errorf("Failed to cleanup transfer expense link for transfer_id=%d: %+v", id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Transfer berhasil dihapus, namun sinkronisasi expense gagal. Silakan cek modul expense")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *transferService) resolveTransferFlagGroup(
|
func (s *transferService) resolveTransferFlagGroup(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
tx *gorm.DB,
|
tx *gorm.DB,
|
||||||
@@ -692,3 +905,179 @@ func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, wa
|
|||||||
|
|
||||||
return uint(projectFlockKandang.Id), nil
|
return uint(projectFlockKandang.Id), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *transferService) ensureTransferAccess(ctx context.Context, id uint, c *fiber.Ctx) error {
|
||||||
|
scope, err := m.ResolveLocationScope(c, s.StockTransferRepo.DB())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !scope.Restrict {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(scope.IDs) == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Transfer not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
if err := s.StockTransferRepo.DB().WithContext(ctx).
|
||||||
|
Table("stock_transfers").
|
||||||
|
Joins("JOIN warehouses w_from ON w_from.id = stock_transfers.from_warehouse_id").
|
||||||
|
Joins("JOIN warehouses w_to ON w_to.id = stock_transfers.to_warehouse_id").
|
||||||
|
Where("stock_transfers.id = ?", id).
|
||||||
|
Where("stock_transfers.deleted_at IS NULL").
|
||||||
|
Where("w_from.location_id IN ? OR w_to.location_id IN ?", scope.IDs, scope.IDs).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Transfer not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *transferService) ensureNoDownstreamConsumptionForDelete(ctx context.Context, tx *gorm.DB, detailIDs []uint64) error {
|
||||||
|
if len(detailIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db := s.StockTransferRepo.DB().WithContext(ctx)
|
||||||
|
if tx != nil {
|
||||||
|
db = tx.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
type downstreamRow struct {
|
||||||
|
UsableType string `gorm:"column:usable_type"`
|
||||||
|
UsableID uint64 `gorm:"column:usable_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []downstreamRow
|
||||||
|
if err := db.Table("stock_allocations").
|
||||||
|
Select("usable_type, usable_id").
|
||||||
|
Where("stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
|
||||||
|
Where("stockable_id IN ?", detailIDs).
|
||||||
|
Where("status = ?", entity.StockAllocationStatusActive).
|
||||||
|
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Group("usable_type, usable_id").
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to validate downstream stock transfer consumption: %+v", err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transaksi turunan transfer stock")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyMap := make(map[string]map[uint64]struct{})
|
||||||
|
for _, row := range rows {
|
||||||
|
label := mapTransferDownstreamUsableLabel(row.UsableType)
|
||||||
|
if _, ok := dependencyMap[label]; !ok {
|
||||||
|
dependencyMap[label] = make(map[uint64]struct{})
|
||||||
|
}
|
||||||
|
dependencyMap[label][row.UsableID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
labels := make([]string, 0, len(dependencyMap))
|
||||||
|
for label := range dependencyMap {
|
||||||
|
labels = append(labels, label)
|
||||||
|
}
|
||||||
|
sort.Strings(labels)
|
||||||
|
|
||||||
|
details := make([]string, 0, len(labels))
|
||||||
|
for _, label := range labels {
|
||||||
|
ids := sortedUint64Keys(dependencyMap[label])
|
||||||
|
details = append(details, fmt.Sprintf("%s=%s", label, joinUint64(ids)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("%s Dependensi aktif: %s.", transferDeleteDownstreamGuardMessage, strings.Join(details, ", ")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapTransferDownstreamUsableLabel(usableType string) string {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(usableType)) {
|
||||||
|
case fifo.UsableKeyRecordingStock.String(), fifo.UsableKeyRecordingDepletion.String():
|
||||||
|
return "Recording"
|
||||||
|
case fifo.UsableKeyProjectChickin.String():
|
||||||
|
return "Chickin"
|
||||||
|
case fifo.UsableKeyMarketingDelivery.String():
|
||||||
|
return "Marketing"
|
||||||
|
case fifo.UsableKeyTransferToLayingOut.String():
|
||||||
|
return "TransferToLaying"
|
||||||
|
case fifo.UsableKeyStockTransferOut.String():
|
||||||
|
return "TransferStock"
|
||||||
|
case fifo.UsableKeyAdjustmentOut.String():
|
||||||
|
return "Adjustment"
|
||||||
|
default:
|
||||||
|
return strings.ToUpper(strings.TrimSpace(usableType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortedUint64Keys(input map[uint64]struct{}) []uint64 {
|
||||||
|
if len(input) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]uint64, 0, len(input))
|
||||||
|
for id := range input {
|
||||||
|
if id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, id)
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinUint64(values []uint64) string {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
parts := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d", value))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "|")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *transferService) appendStockLog(
|
||||||
|
ctx context.Context,
|
||||||
|
stockLogRepo rStockLogs.StockLogRepository,
|
||||||
|
productWarehouseID uint,
|
||||||
|
actorID uint,
|
||||||
|
increase float64,
|
||||||
|
decrease float64,
|
||||||
|
loggableID uint,
|
||||||
|
notes string,
|
||||||
|
) error {
|
||||||
|
if productWarehouseID == 0 || (increase <= 1e-6 && decrease <= 1e-6) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stockLog := &entity.StockLog{
|
||||||
|
ProductWarehouseId: productWarehouseID,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
Increase: increase,
|
||||||
|
Decrease: decrease,
|
||||||
|
LoggableType: string(utils.StockLogTypeTransfer),
|
||||||
|
LoggableId: loggableID,
|
||||||
|
Notes: notes,
|
||||||
|
}
|
||||||
|
|
||||||
|
stockLogs, err := stockLogRepo.GetByProductWarehouse(ctx, productWarehouseID, 1)
|
||||||
|
if err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
||||||
|
}
|
||||||
|
if len(stockLogs) > 0 {
|
||||||
|
latestStockLog := stockLogs[0]
|
||||||
|
stockLog.Stock = latestStockLog.Stock + increase - decrease
|
||||||
|
} else {
|
||||||
|
stockLog.Stock = increase - decrease
|
||||||
|
}
|
||||||
|
if err := stockLogRepo.CreateOne(ctx, stockLog, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat stock log saat delete transfer")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
chickinDeletePopulationGuardMessage = "Chickin tidak dapat dihapus karena masih memiliki population aktif"
|
chickinDeletePopulationGuardMessage = "Chickin tidak dapat dihapus karena masih memiliki population aktif"
|
||||||
chickinDeleteDownstreamGuardMessage = "Chickin tidak bisa dihapus karena masih dipakai oleh transaksi turunan. Hapus/unexecute Marketing, Recording, dan Transfer to Laying terlebih dahulu."
|
chickinDeleteDownstreamGuardMessage = "Chickin tidak bisa dihapus karena masih dipakai oleh transaksi turunan. Hapus/unexecute Marketing, Recording, Transfer, Adjustment, dan Transfer to Laying terlebih dahulu."
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChickinService interface {
|
type ChickinService interface {
|
||||||
@@ -264,16 +264,16 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
availableQty = 0
|
availableQty = 0
|
||||||
}
|
}
|
||||||
if flockCategory == strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) {
|
if flockCategory == strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) {
|
||||||
transferAvailable, err := s.resolveLayingTransferAvailableQty(c.Context(), nil, req.ProjectFlockKandangId, chickinReq.ProductWarehouseId)
|
sourceAvailable, err := s.resolveLayingSourceAvailableQty(c.Context(), nil, chickinReq.ProductWarehouseId, &chickinDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to resolve laying transfer availability for pfk=%d pw=%d: %+v", req.ProjectFlockKandangId, chickinReq.ProductWarehouseId, err)
|
s.Log.Errorf("Failed to resolve laying transfer availability for pfk=%d pw=%d: %+v", req.ProjectFlockKandangId, chickinReq.ProductWarehouseId, err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi stok transfer laying")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi stok transfer laying")
|
||||||
}
|
}
|
||||||
if transferAvailable < 0 {
|
if sourceAvailable < 0 {
|
||||||
transferAvailable = 0
|
sourceAvailable = 0
|
||||||
}
|
}
|
||||||
if transferAvailable < availableQty {
|
if sourceAvailable < availableQty {
|
||||||
availableQty = transferAvailable
|
availableQty = sourceAvailable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,36 +554,44 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *chickinService) resolveLayingTransferAvailableQty(ctx context.Context, tx *gorm.DB, targetProjectFlockKandangID, productWarehouseID uint) (float64, error) {
|
func (s *chickinService) resolveLayingSourceAvailableQty(ctx context.Context, tx *gorm.DB, productWarehouseID uint, asOf *time.Time) (float64, error) {
|
||||||
if targetProjectFlockKandangID == 0 || productWarehouseID == 0 {
|
if productWarehouseID == 0 || s.FifoStockV2Svc == nil {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
db := s.Repository.DB().WithContext(ctx)
|
db := s.Repository.DB()
|
||||||
if tx != nil {
|
if tx != nil {
|
||||||
db = tx.WithContext(ctx)
|
db = tx
|
||||||
}
|
}
|
||||||
|
|
||||||
var available float64
|
flagGroupCode, err := resolveChickinFlagGroupByProductWarehouse(ctx, db, productWarehouseID)
|
||||||
err := db.Table("laying_transfer_targets ltt").
|
|
||||||
Select("COALESCE(SUM(GREATEST(0, COALESCE(ltt.total_qty,0) - COALESCE(ltt.total_used,0))), 0) AS available").
|
|
||||||
Joins("JOIN laying_transfers lt ON lt.id = ltt.laying_transfer_id AND lt.deleted_at IS NULL").
|
|
||||||
Where("ltt.deleted_at IS NULL").
|
|
||||||
Where("ltt.target_project_flock_kandang_id = ?", targetProjectFlockKandangID).
|
|
||||||
Where("ltt.product_warehouse_id = ?", productWarehouseID).
|
|
||||||
Where("lt.executed_at IS NOT NULL").
|
|
||||||
Where(`(
|
|
||||||
SELECT a.action
|
|
||||||
FROM approvals a
|
|
||||||
WHERE a.approvable_type = ?
|
|
||||||
AND a.approvable_id = lt.id
|
|
||||||
ORDER BY a.id DESC
|
|
||||||
LIMIT 1
|
|
||||||
) = ?`, string(utils.ApprovalWorkflowTransferToLaying), entity.ApprovalActionApproved).
|
|
||||||
Scan(&available).Error
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(flagGroupCode) == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gatherRows, err := s.FifoStockV2Svc.Gather(ctx, commonSvc.FifoStockV2GatherRequest{
|
||||||
|
FlagGroupCode: flagGroupCode,
|
||||||
|
Lane: commonSvc.FifoStockV2Lane("STOCKABLE"),
|
||||||
|
AllocationPurpose: entity.StockAllocationPurposeConsume,
|
||||||
|
ProductWarehouseID: productWarehouseID,
|
||||||
|
AsOf: asOf,
|
||||||
|
Limit: 10000,
|
||||||
|
Tx: tx,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
available := 0.0
|
||||||
|
for _, row := range gatherRows {
|
||||||
|
if row.AvailableQuantity <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
available += row.AvailableQuantity
|
||||||
|
}
|
||||||
return available, nil
|
return available, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -650,6 +658,8 @@ func (s *chickinService) ensureNoDownstreamConsumptionForDelete(ctx context.Cont
|
|||||||
Where("sa.usable_type IN ?", []string{
|
Where("sa.usable_type IN ?", []string{
|
||||||
fifo.UsableKeyMarketingDelivery.String(),
|
fifo.UsableKeyMarketingDelivery.String(),
|
||||||
fifo.UsableKeyRecordingDepletion.String(),
|
fifo.UsableKeyRecordingDepletion.String(),
|
||||||
|
fifo.UsableKeyStockTransferOut.String(),
|
||||||
|
fifo.UsableKeyAdjustmentOut.String(),
|
||||||
fifo.UsableKeyTransferToLayingOut.String(),
|
fifo.UsableKeyTransferToLayingOut.String(),
|
||||||
}).
|
}).
|
||||||
Group("sa.usable_type, sa.usable_id").
|
Group("sa.usable_type, sa.usable_id").
|
||||||
@@ -664,6 +674,8 @@ func (s *chickinService) ensureNoDownstreamConsumptionForDelete(ctx context.Cont
|
|||||||
|
|
||||||
marketingIDs := make(map[uint]struct{})
|
marketingIDs := make(map[uint]struct{})
|
||||||
recordingIDs := make(map[uint]struct{})
|
recordingIDs := make(map[uint]struct{})
|
||||||
|
transferIDs := make(map[uint]struct{})
|
||||||
|
adjustmentIDs := make(map[uint]struct{})
|
||||||
transferLayingIDs := make(map[uint]struct{})
|
transferLayingIDs := make(map[uint]struct{})
|
||||||
|
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
@@ -672,18 +684,28 @@ func (s *chickinService) ensureNoDownstreamConsumptionForDelete(ctx context.Cont
|
|||||||
marketingIDs[row.UsableID] = struct{}{}
|
marketingIDs[row.UsableID] = struct{}{}
|
||||||
case fifo.UsableKeyRecordingDepletion.String():
|
case fifo.UsableKeyRecordingDepletion.String():
|
||||||
recordingIDs[row.UsableID] = struct{}{}
|
recordingIDs[row.UsableID] = struct{}{}
|
||||||
|
case fifo.UsableKeyStockTransferOut.String():
|
||||||
|
transferIDs[row.UsableID] = struct{}{}
|
||||||
|
case fifo.UsableKeyAdjustmentOut.String():
|
||||||
|
adjustmentIDs[row.UsableID] = struct{}{}
|
||||||
case fifo.UsableKeyTransferToLayingOut.String():
|
case fifo.UsableKeyTransferToLayingOut.String():
|
||||||
transferLayingIDs[row.UsableID] = struct{}{}
|
transferLayingIDs[row.UsableID] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
details := make([]string, 0, 3)
|
details := make([]string, 0, 5)
|
||||||
if ids := sortedIDs(marketingIDs); len(ids) > 0 {
|
if ids := sortedIDs(marketingIDs); len(ids) > 0 {
|
||||||
details = append(details, fmt.Sprintf("Marketing=%s", joinUint(ids)))
|
details = append(details, fmt.Sprintf("Marketing=%s", joinUint(ids)))
|
||||||
}
|
}
|
||||||
if ids := sortedIDs(recordingIDs); len(ids) > 0 {
|
if ids := sortedIDs(recordingIDs); len(ids) > 0 {
|
||||||
details = append(details, fmt.Sprintf("Recording=%s", joinUint(ids)))
|
details = append(details, fmt.Sprintf("Recording=%s", joinUint(ids)))
|
||||||
}
|
}
|
||||||
|
if ids := sortedIDs(transferIDs); len(ids) > 0 {
|
||||||
|
details = append(details, fmt.Sprintf("Transfer=%s", joinUint(ids)))
|
||||||
|
}
|
||||||
|
if ids := sortedIDs(adjustmentIDs); len(ids) > 0 {
|
||||||
|
details = append(details, fmt.Sprintf("Adjustment=%s", joinUint(ids)))
|
||||||
|
}
|
||||||
if ids := sortedIDs(transferLayingIDs); len(ids) > 0 {
|
if ids := sortedIDs(transferLayingIDs); len(ids) > 0 {
|
||||||
details = append(details, fmt.Sprintf("TransferToLaying=%s", joinUint(ids)))
|
details = append(details, fmt.Sprintf("TransferToLaying=%s", joinUint(ids)))
|
||||||
}
|
}
|
||||||
@@ -1292,17 +1314,8 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !shouldRestoreWarehouseQty {
|
if !shouldRestoreWarehouseQty {
|
||||||
var affectedTransferTargetIDs []uint
|
affectedStockables, err := s.listActiveConsumeStockableRefsByUsable(ctx, tx, chickin.Id)
|
||||||
if err := tx.WithContext(ctx).
|
if err != nil {
|
||||||
Model(&entity.StockAllocation{}).
|
|
||||||
Where("usable_type = ? AND usable_id = ? AND status = ? AND allocation_purpose = ? AND stockable_type = ?",
|
|
||||||
fifo.UsableKeyProjectChickin.String(),
|
|
||||||
chickin.Id,
|
|
||||||
entity.StockAllocationStatusActive,
|
|
||||||
entity.StockAllocationPurposeConsume,
|
|
||||||
fifo.StockableKeyTransferToLayingIn.String(),
|
|
||||||
).
|
|
||||||
Pluck("stockable_id", &affectedTransferTargetIDs).Error; err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1325,12 +1338,15 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.Log.Infof(
|
s.Log.Infof(
|
||||||
"Release chickin stock laying id=%d released_consume_alloc=%d transfer_targets=%d",
|
"Release chickin stock laying id=%d released_consume_alloc=%d transfer_targets=%d stock_transfer_sources=%d purchase_sources=%d adjustment_sources=%d",
|
||||||
chickin.Id,
|
chickin.Id,
|
||||||
releaseResult.RowsAffected,
|
releaseResult.RowsAffected,
|
||||||
len(affectedTransferTargetIDs),
|
len(affectedStockables[fifo.StockableKeyTransferToLayingIn.String()]),
|
||||||
|
len(affectedStockables[fifo.StockableKeyStockTransferIn.String()]),
|
||||||
|
len(affectedStockables[fifo.StockableKeyPurchaseItems.String()]),
|
||||||
|
len(affectedStockables[fifo.StockableKeyAdjustmentIn.String()]),
|
||||||
)
|
)
|
||||||
if err := s.resyncTransferTargetUsageFromAllocations(ctx, tx, affectedTransferTargetIDs); err != nil {
|
if err := s.resyncStockableSourceUsageAfterRelease(ctx, tx, affectedStockables); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
|
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
|
||||||
@@ -1465,63 +1481,179 @@ func (s *chickinService) logWarehouseQtySnapshot(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *chickinService) resyncTransferTargetUsageFromAllocations(ctx context.Context, tx *gorm.DB, transferTargetIDs []uint) error {
|
func (s *chickinService) listActiveConsumeStockableRefsByUsable(ctx context.Context, tx *gorm.DB, chickinID uint) (map[string][]uint, error) {
|
||||||
if tx == nil || len(transferTargetIDs) == 0 {
|
result := map[string][]uint{
|
||||||
return nil
|
fifo.StockableKeyTransferToLayingIn.String(): nil,
|
||||||
|
fifo.StockableKeyStockTransferIn.String(): nil,
|
||||||
|
fifo.StockableKeyPurchaseItems.String(): nil,
|
||||||
|
fifo.StockableKeyAdjustmentIn.String(): nil,
|
||||||
|
}
|
||||||
|
if tx == nil || chickinID == 0 {
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
unique := make([]uint, 0, len(transferTargetIDs))
|
type row struct {
|
||||||
seen := make(map[uint]struct{}, len(transferTargetIDs))
|
StockableType string `gorm:"column:stockable_type"`
|
||||||
for _, id := range transferTargetIDs {
|
StockableID uint `gorm:"column:stockable_id"`
|
||||||
if id == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[id]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[id] = struct{}{}
|
|
||||||
unique = append(unique, id)
|
|
||||||
}
|
}
|
||||||
if len(unique) == 0 {
|
var rows []row
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.WithContext(ctx).
|
|
||||||
Model(&entity.LayingTransferTarget{}).
|
|
||||||
Where("id IN ?", unique).
|
|
||||||
Update("total_used", 0).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type usageRow struct {
|
|
||||||
StockableID uint `gorm:"column:stockable_id"`
|
|
||||||
Used float64 `gorm:"column:used"`
|
|
||||||
}
|
|
||||||
var usageRows []usageRow
|
|
||||||
if err := tx.WithContext(ctx).
|
if err := tx.WithContext(ctx).
|
||||||
Table("stock_allocations").
|
Table("stock_allocations").
|
||||||
Select("stockable_id, COALESCE(SUM(qty), 0) AS used").
|
Select("stockable_type, stockable_id").
|
||||||
Where("stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
|
Where("usable_type = ? AND usable_id = ? AND status = ? AND allocation_purpose = ?",
|
||||||
Where("status = ?", entity.StockAllocationStatusActive).
|
fifo.UsableKeyProjectChickin.String(),
|
||||||
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
chickinID,
|
||||||
Where("stockable_id IN ?", unique).
|
entity.StockAllocationStatusActive,
|
||||||
Group("stockable_id").
|
entity.StockAllocationPurposeConsume,
|
||||||
Scan(&usageRows).Error; err != nil {
|
).
|
||||||
|
Where("stockable_type IN ?", []string{
|
||||||
|
fifo.StockableKeyTransferToLayingIn.String(),
|
||||||
|
fifo.StockableKeyStockTransferIn.String(),
|
||||||
|
fifo.StockableKeyPurchaseItems.String(),
|
||||||
|
fifo.StockableKeyAdjustmentIn.String(),
|
||||||
|
}).
|
||||||
|
Group("stockable_type, stockable_id").
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
if row.StockableID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[row.StockableType] = append(result[row.StockableType], row.StockableID)
|
||||||
|
}
|
||||||
|
for key, ids := range result {
|
||||||
|
result[key] = uniqueUint(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *chickinService) resyncStockableSourceUsageAfterRelease(ctx context.Context, tx *gorm.DB, stockableRefs map[string][]uint) error {
|
||||||
|
if tx == nil || len(stockableRefs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.resetAndResyncUsedQuantity(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
"laying_transfer_targets",
|
||||||
|
"id",
|
||||||
|
"total_used",
|
||||||
|
fifo.StockableKeyTransferToLayingIn.String(),
|
||||||
|
stockableRefs[fifo.StockableKeyTransferToLayingIn.String()],
|
||||||
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, row := range usageRows {
|
if err := s.resetAndResyncUsedQuantity(
|
||||||
if err := tx.WithContext(ctx).
|
ctx,
|
||||||
Model(&entity.LayingTransferTarget{}).
|
tx,
|
||||||
Where("id = ?", row.StockableID).
|
"stock_transfer_details",
|
||||||
Update("total_used", row.Used).Error; err != nil {
|
"id",
|
||||||
return err
|
"total_used",
|
||||||
}
|
fifo.StockableKeyStockTransferIn.String(),
|
||||||
|
stockableRefs[fifo.StockableKeyStockTransferIn.String()],
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.resetAndResyncUsedQuantity(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
"purchase_items",
|
||||||
|
"id",
|
||||||
|
"total_used",
|
||||||
|
fifo.StockableKeyPurchaseItems.String(),
|
||||||
|
stockableRefs[fifo.StockableKeyPurchaseItems.String()],
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.resetAndResyncUsedQuantity(
|
||||||
|
ctx,
|
||||||
|
tx,
|
||||||
|
"adjustment_stocks",
|
||||||
|
"id",
|
||||||
|
"total_used",
|
||||||
|
fifo.StockableKeyAdjustmentIn.String(),
|
||||||
|
stockableRefs[fifo.StockableKeyAdjustmentIn.String()],
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *chickinService) resetAndResyncUsedQuantity(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
tableName string,
|
||||||
|
idColumn string,
|
||||||
|
usedColumn string,
|
||||||
|
stockableType string,
|
||||||
|
ids []uint,
|
||||||
|
) error {
|
||||||
|
ids = uniqueUint(ids)
|
||||||
|
if tx == nil || len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.WithContext(ctx).
|
||||||
|
Table(tableName).
|
||||||
|
Where(fmt.Sprintf("%s IN ?", idColumn), ids).
|
||||||
|
Update(usedColumn, 0).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf(`
|
||||||
|
UPDATE %s AS t
|
||||||
|
SET %s = a.used
|
||||||
|
FROM (
|
||||||
|
SELECT stockable_id, COALESCE(SUM(qty), 0) AS used
|
||||||
|
FROM stock_allocations
|
||||||
|
WHERE stockable_type = ?
|
||||||
|
AND status = ?
|
||||||
|
AND allocation_purpose = ?
|
||||||
|
AND stockable_id IN ?
|
||||||
|
GROUP BY stockable_id
|
||||||
|
) AS a
|
||||||
|
WHERE t.%s = a.stockable_id
|
||||||
|
`, tableName, usedColumn, idColumn)
|
||||||
|
|
||||||
|
if err := tx.WithContext(ctx).Exec(
|
||||||
|
query,
|
||||||
|
stockableType,
|
||||||
|
entity.StockAllocationStatusActive,
|
||||||
|
entity.StockAllocationPurposeConsume,
|
||||||
|
ids,
|
||||||
|
).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueUint(values []uint) []uint {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]uint, 0, len(values))
|
||||||
|
seen := make(map[uint]struct{}, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
if value == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[value]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[value] = struct{}{}
|
||||||
|
out = append(out, value)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeDateOnlyUTC(value time.Time) time.Time {
|
func normalizeDateOnlyUTC(value time.Time) time.Time {
|
||||||
if value.IsZero() {
|
if value.IsZero() {
|
||||||
return time.Time{}
|
return time.Time{}
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
|
|
||||||
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
|
||||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
|
||||||
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
@@ -33,13 +33,14 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB
|
|||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
|
fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log)
|
||||||
// register workflow steps for chickin approvals
|
// register workflow steps for chickin approvals
|
||||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
|
||||||
panic(fmt.Sprintf("failed to register chickin approval workflow: %v", err))
|
panic(fmt.Sprintf("failed to register chickin approval workflow: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
expenseRepo := rExpense.NewExpenseRepository(db)
|
expenseRepo := rExpense.NewExpenseRepository(db)
|
||||||
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, expenseRepo, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo,kandangRepo, validate)
|
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, fifoStockV2Service, expenseRepo, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, kandangRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ProjectFlockKandangRoutes(router, userService, projectFlockKandangService)
|
ProjectFlockKandangRoutes(router, userService, projectFlockKandangService)
|
||||||
|
|||||||
+89
-1
@@ -1,8 +1,10 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -35,6 +37,7 @@ type projectFlockKandangService struct {
|
|||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
Repository repository.ProjectFlockKandangRepository
|
Repository repository.ProjectFlockKandangRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
FifoStockV2Svc commonSvc.FifoStockV2Service
|
||||||
ExpenseRepo expenseRepo.ExpenseRepository
|
ExpenseRepo expenseRepo.ExpenseRepository
|
||||||
WarehouseRepo rWarehouse.WarehouseRepository
|
WarehouseRepo rWarehouse.WarehouseRepository
|
||||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||||
@@ -69,12 +72,13 @@ type ExpenseSummary struct {
|
|||||||
Reference string `json:"reference_number"`
|
Reference string `json:"reference_number"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, expenseRepo expenseRepo.ExpenseRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, kandangRepo kandangRepo.KandangRepository, validate *validator.Validate) ProjectFlockKandangService {
|
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, fifoStockV2Svc commonSvc.FifoStockV2Service, expenseRepo expenseRepo.ExpenseRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, kandangRepo kandangRepo.KandangRepository, validate *validator.Validate) ProjectFlockKandangService {
|
||||||
return &projectFlockKandangService{
|
return &projectFlockKandangService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
|
FifoStockV2Svc: fifoStockV2Svc,
|
||||||
ExpenseRepo: expenseRepo,
|
ExpenseRepo: expenseRepo,
|
||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
@@ -671,7 +675,91 @@ func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehous
|
|||||||
if availableQty < 0 {
|
if availableQty < 0 {
|
||||||
availableQty = 0
|
availableQty = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sourceAvailable, err := s.resolveLayingSourceAvailableQty(c.Context(), nil, productWarehouse.Id, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if sourceAvailable < availableQty {
|
||||||
|
availableQty = sourceAvailable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return availableQty, nil
|
return availableQty, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s projectFlockKandangService) resolveLayingSourceAvailableQty(ctx context.Context, tx *gorm.DB, productWarehouseID uint, asOf *time.Time) (float64, error) {
|
||||||
|
if productWarehouseID == 0 || s.FifoStockV2Svc == nil {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
flagGroupCode, err := s.resolveFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(flagGroupCode) == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gatherRows, err := s.FifoStockV2Svc.Gather(ctx, commonSvc.FifoStockV2GatherRequest{
|
||||||
|
FlagGroupCode: flagGroupCode,
|
||||||
|
Lane: commonSvc.FifoStockV2Lane("STOCKABLE"),
|
||||||
|
AllocationPurpose: entity.StockAllocationPurposeConsume,
|
||||||
|
ProductWarehouseID: productWarehouseID,
|
||||||
|
AsOf: asOf,
|
||||||
|
Limit: 10000,
|
||||||
|
Tx: tx,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
total := 0.0
|
||||||
|
for _, row := range gatherRows {
|
||||||
|
if row.AvailableQuantity <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total += row.AvailableQuantity
|
||||||
|
}
|
||||||
|
return math.Max(total, 0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s projectFlockKandangService) resolveFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) {
|
||||||
|
type row struct {
|
||||||
|
FlagGroupCode string `gorm:"column:flag_group_code"`
|
||||||
|
}
|
||||||
|
selected := row{}
|
||||||
|
|
||||||
|
db := s.Repository.DB()
|
||||||
|
if tx != nil {
|
||||||
|
db = tx
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.WithContext(ctx).
|
||||||
|
Table("fifo_stock_v2_route_rules rr").
|
||||||
|
Select("rr.flag_group_code").
|
||||||
|
Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
|
||||||
|
Where("rr.is_active = TRUE").
|
||||||
|
Where("rr.lane = 'STOCKABLE'").
|
||||||
|
Where(`
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM product_warehouses pw
|
||||||
|
JOIN flags f ON f.flagable_id = pw.product_id
|
||||||
|
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
|
||||||
|
WHERE pw.id = ?
|
||||||
|
AND f.flagable_type = ?
|
||||||
|
AND fm.flag_group_code = rr.flag_group_code
|
||||||
|
)
|
||||||
|
`, productWarehouseID, entity.FlagableTypeProduct).
|
||||||
|
Order("fg.priority ASC, rr.id ASC").
|
||||||
|
Limit(1).
|
||||||
|
Take(&selected).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(selected.FlagGroupCode), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
productWarehouseRepo,
|
productWarehouseRepo,
|
||||||
warehouseRepo,
|
warehouseRepo,
|
||||||
approvalService,
|
approvalService,
|
||||||
fifoService,
|
|
||||||
fifoStockV2Service,
|
fifoStockV2Service,
|
||||||
validate,
|
validate,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
|
|||||||
productWarehouseRepo,
|
productWarehouseRepo,
|
||||||
warehouseRepo,
|
warehouseRepo,
|
||||||
approvalService,
|
approvalService,
|
||||||
fifoService,
|
|
||||||
fifoStockV2Service,
|
fifoStockV2Service,
|
||||||
validate,
|
validate,
|
||||||
)
|
)
|
||||||
|
|||||||
+132
@@ -2,15 +2,22 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LayingTransferTargetRepository interface {
|
type LayingTransferTargetRepository interface {
|
||||||
repository.BaseRepository[entity.LayingTransferTarget]
|
repository.BaseRepository[entity.LayingTransferTarget]
|
||||||
GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferTarget, error)
|
GetByLayingTransferId(ctx context.Context, layingTransferId uint) ([]entity.LayingTransferTarget, error)
|
||||||
|
GetActiveDownstreamConsumptions(ctx context.Context, targetIDs []uint) ([]TargetDownstreamConsumption, error)
|
||||||
|
GetEarliestRecordingDateByTarget(ctx context.Context, targetProjectFlockKandangID uint, sinceDate time.Time) (*time.Time, error)
|
||||||
|
CountActiveTransferSourceConsumeAllocations(ctx context.Context, transferID uint, productWarehouseID uint) (int64, error)
|
||||||
|
SyncPopulationUsageByProjectFlockKandang(ctx context.Context, projectFlockKandangID uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type LayingTransferTargetRepositoryImpl struct {
|
type LayingTransferTargetRepositoryImpl struct {
|
||||||
@@ -18,6 +25,11 @@ type LayingTransferTargetRepositoryImpl struct {
|
|||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TargetDownstreamConsumption struct {
|
||||||
|
UsableType string `gorm:"column:usable_type"`
|
||||||
|
UsableID uint `gorm:"column:usable_id"`
|
||||||
|
}
|
||||||
|
|
||||||
func NewLayingTransferTargetRepository(db *gorm.DB) LayingTransferTargetRepository {
|
func NewLayingTransferTargetRepository(db *gorm.DB) LayingTransferTargetRepository {
|
||||||
return &LayingTransferTargetRepositoryImpl{
|
return &LayingTransferTargetRepositoryImpl{
|
||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransferTarget](db),
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransferTarget](db),
|
||||||
@@ -36,3 +48,123 @@ func (r *LayingTransferTargetRepositoryImpl) GetByLayingTransferId(ctx context.C
|
|||||||
}
|
}
|
||||||
return targets, nil
|
return targets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LayingTransferTargetRepositoryImpl) GetActiveDownstreamConsumptions(ctx context.Context, targetIDs []uint) ([]TargetDownstreamConsumption, error) {
|
||||||
|
if len(targetIDs) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []TargetDownstreamConsumption
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("stock_allocations").
|
||||||
|
Select("usable_type, usable_id").
|
||||||
|
Where("stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
|
||||||
|
Where("stockable_id IN ?", targetIDs).
|
||||||
|
Where("status = ?", entity.StockAllocationStatusActive).
|
||||||
|
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Group("usable_type, usable_id").
|
||||||
|
Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LayingTransferTargetRepositoryImpl) GetEarliestRecordingDateByTarget(ctx context.Context, targetProjectFlockKandangID uint, sinceDate time.Time) (*time.Time, error) {
|
||||||
|
if targetProjectFlockKandangID == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var earliest entity.Recording
|
||||||
|
query := r.db.WithContext(ctx).
|
||||||
|
Model(&entity.Recording{}).
|
||||||
|
Where("project_flock_kandangs_id = ?", targetProjectFlockKandangID).
|
||||||
|
Where("deleted_at IS NULL")
|
||||||
|
if !sinceDate.IsZero() {
|
||||||
|
query = query.Where("record_datetime >= ?", sinceDate)
|
||||||
|
}
|
||||||
|
if err := query.Order("record_datetime ASC").Limit(1).Take(&earliest).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
d := earliest.RecordDatetime.UTC()
|
||||||
|
return &d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LayingTransferTargetRepositoryImpl) CountActiveTransferSourceConsumeAllocations(ctx context.Context, transferID uint, productWarehouseID uint) (int64, error) {
|
||||||
|
if transferID == 0 || productWarehouseID == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Model(&entity.StockAllocation{}).
|
||||||
|
Where("product_warehouse_id = ?", productWarehouseID).
|
||||||
|
Where("usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()).
|
||||||
|
Where("usable_id = ?", transferID).
|
||||||
|
Where("status = ?", entity.StockAllocationStatusActive).
|
||||||
|
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||||
|
Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LayingTransferTargetRepositoryImpl) SyncPopulationUsageByProjectFlockKandang(ctx context.Context, projectFlockKandangID uint) error {
|
||||||
|
if projectFlockKandangID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var populationIDs []uint
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Table("project_flock_populations pfp").
|
||||||
|
Select("pfp.id").
|
||||||
|
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
||||||
|
Where("pc.project_flock_kandang_id = ?", projectFlockKandangID).
|
||||||
|
Pluck("pfp.id", &populationIDs).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(populationIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type usageRow struct {
|
||||||
|
StockableID uint `gorm:"column:stockable_id"`
|
||||||
|
Used float64 `gorm:"column:used"`
|
||||||
|
}
|
||||||
|
var usageRows []usageRow
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Table("stock_allocations").
|
||||||
|
Select("stockable_id, COALESCE(SUM(qty), 0) AS used").
|
||||||
|
Where("stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
||||||
|
Where("status = ?", entity.StockAllocationStatusActive).
|
||||||
|
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||||
|
Where("stockable_id IN ?", populationIDs).
|
||||||
|
Group("stockable_id").
|
||||||
|
Scan(&usageRows).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Model(&entity.ProjectFlockPopulation{}).
|
||||||
|
Where("id IN ?", populationIDs).
|
||||||
|
Update("total_used_qty", 0).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range usageRows {
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Model(&entity.ProjectFlockPopulation{}).
|
||||||
|
Where("id = ?", row.StockableID).
|
||||||
|
Update("total_used_qty", row.Used).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
+162
-112
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -56,12 +57,12 @@ type transferLayingService struct {
|
|||||||
WarehouseRepo rWarehouse.WarehouseRepository
|
WarehouseRepo rWarehouse.WarehouseRepository
|
||||||
StockLogRepo rStockLogs.StockLogRepository
|
StockLogRepo rStockLogs.StockLogRepository
|
||||||
ApprovalService commonSvc.ApprovalService
|
ApprovalService commonSvc.ApprovalService
|
||||||
FifoSvc commonSvc.FifoService
|
|
||||||
FifoStockV2Svc commonSvc.FifoStockV2Service
|
FifoStockV2Svc commonSvc.FifoStockV2Service
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
transferToLayingFlagGroupCode = "AYAM"
|
transferToLayingFlagGroupCode = "AYAM"
|
||||||
|
transferLayingDeleteDownstreamGuardMessage = "Transfer laying tidak dapat dihapus karena stok target transfer sudah dipakai transaksi turunan. Hapus dependensi terkait secara manual terlebih dahulu."
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewTransferLayingService(
|
func NewTransferLayingService(
|
||||||
@@ -74,7 +75,6 @@ func NewTransferLayingService(
|
|||||||
productWarehouseRepo rInventory.ProductWarehouseRepository,
|
productWarehouseRepo rInventory.ProductWarehouseRepository,
|
||||||
warehouseRepo rWarehouse.WarehouseRepository,
|
warehouseRepo rWarehouse.WarehouseRepository,
|
||||||
approvalService commonSvc.ApprovalService,
|
approvalService commonSvc.ApprovalService,
|
||||||
fifoSvc commonSvc.FifoService,
|
|
||||||
fifoStockV2Svc commonSvc.FifoStockV2Service,
|
fifoStockV2Svc commonSvc.FifoStockV2Service,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
) TransferLayingService {
|
) TransferLayingService {
|
||||||
@@ -91,7 +91,6 @@ func NewTransferLayingService(
|
|||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
|
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
|
||||||
ApprovalService: approvalService,
|
ApprovalService: approvalService,
|
||||||
FifoSvc: fifoSvc,
|
|
||||||
FifoStockV2Svc: fifoStockV2Svc,
|
FifoStockV2Svc: fifoStockV2Svc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -610,6 +609,9 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
if isLegacyTransfer(transfer) {
|
if isLegacyTransfer(transfer) {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer laying legacy %s tidak dapat dihapus", transfer.TransferNumber))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Transfer laying legacy %s tidak dapat dihapus", transfer.TransferNumber))
|
||||||
}
|
}
|
||||||
|
if err := s.ensureNoDownstreamConsumptionForDelete(c.Context(), nil, transfer.TransferNumber, transfer.Targets); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
|
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
|
||||||
|
|
||||||
@@ -635,6 +637,16 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
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)
|
||||||
|
|
||||||
|
// Lock header row to keep delete deterministic after single downstream guard check.
|
||||||
|
if _, err := repoTx.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Clauses(clause.Locking{Strength: "UPDATE"})
|
||||||
|
}); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
|
||||||
|
}
|
||||||
|
|
||||||
if err := repoTx.DeleteOne(c.Context(), id); err != nil {
|
if err := repoTx.DeleteOne(c.Context(), id); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
|
||||||
}
|
}
|
||||||
@@ -1053,6 +1065,11 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type targetReflowKey struct {
|
||||||
|
productWarehouseID uint
|
||||||
|
}
|
||||||
|
targetReflow := make(map[targetReflowKey]struct{})
|
||||||
|
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
if target.ProductWarehouseId == nil || *target.ProductWarehouseId == 0 {
|
if target.ProductWarehouseId == nil || *target.ProductWarehouseId == 0 {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", transfer.Id))
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", transfer.Id))
|
||||||
@@ -1060,15 +1077,6 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT
|
|||||||
if target.TotalQty <= 0 {
|
if target.TotalQty <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := s.FifoSvc.AdjustStockableQuantity(c.Context(), commonSvc.StockAdjustRequest{
|
|
||||||
StockableKey: fifo.StockableKeyTransferToLayingIn,
|
|
||||||
StockableID: target.Id,
|
|
||||||
ProductWarehouseID: *target.ProductWarehouseId,
|
|
||||||
Quantity: -target.TotalQty,
|
|
||||||
Tx: dbTransaction,
|
|
||||||
}); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback stok target transfer laying: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
stockLogDecrease := &entity.StockLog{
|
stockLogDecrease := &entity.StockLog{
|
||||||
ProductWarehouseId: *target.ProductWarehouseId,
|
ProductWarehouseId: *target.ProductWarehouseId,
|
||||||
@@ -1092,6 +1100,20 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT
|
|||||||
if err := stockLogRepoTx.CreateOne(c.Context(), stockLogDecrease, nil); err != nil {
|
if err := stockLogRepoTx.CreateOne(c.Context(), stockLogDecrease, nil); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar target saat unexecute")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar target saat unexecute")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := targetRepoTx.PatchOne(c.Context(), target.Id, map[string]any{
|
||||||
|
"total_qty": 0,
|
||||||
|
"total_used": 0,
|
||||||
|
}, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal rollback kuantitas target transfer laying")
|
||||||
|
}
|
||||||
|
targetReflow[targetReflowKey{productWarehouseID: *target.ProductWarehouseId}] = struct{}{}
|
||||||
|
}
|
||||||
|
asOf := normalizeDateOnlyUTC(transfer.TransferDate)
|
||||||
|
for key := range targetReflow {
|
||||||
|
if err := reflowTransferLayingScope(c.Context(), s.FifoStockV2Svc, dbTransaction, key.productWarehouseID, &asOf); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback FIFO v2 target transfer laying: %v", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rollbackResult, err := s.FifoStockV2Svc.Rollback(c.Context(), commonSvc.FifoStockV2RollbackRequest{
|
rollbackResult, err := s.FifoStockV2Svc.Rollback(c.Context(), commonSvc.FifoStockV2RollbackRequest{
|
||||||
@@ -1229,9 +1251,6 @@ func (s *transferLayingService) executeApprovedTransferMovement(
|
|||||||
if s.FifoStockV2Svc == nil {
|
if s.FifoStockV2Svc == nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
|
return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available")
|
||||||
}
|
}
|
||||||
if s.FifoSvc == nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "FIFO service is not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx)
|
stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx)
|
||||||
targetRepoTx := repository.NewLayingTransferTargetRepository(tx)
|
targetRepoTx := repository.NewLayingTransferTargetRepository(tx)
|
||||||
@@ -1327,29 +1346,22 @@ func (s *transferLayingService) executeApprovedTransferMovement(
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok keluar")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type targetReflowKey struct {
|
||||||
|
productWarehouseID uint
|
||||||
|
}
|
||||||
|
targetReflow := make(map[targetReflowKey]struct{})
|
||||||
|
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
if target.ProductWarehouseId == nil {
|
if target.ProductWarehouseId == nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", transfer.Id))
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", transfer.Id))
|
||||||
}
|
}
|
||||||
|
|
||||||
note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber)
|
|
||||||
_, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
|
||||||
StockableKey: fifo.StockableKeyTransferToLayingIn,
|
|
||||||
StockableID: target.Id,
|
|
||||||
ProductWarehouseID: *target.ProductWarehouseId,
|
|
||||||
Quantity: target.TotalQty,
|
|
||||||
Note: ¬e,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal replenish stock ke target warehouse: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := targetRepoTx.PatchOne(ctx, target.Id, map[string]any{
|
if err := targetRepoTx.PatchOne(ctx, target.Id, map[string]any{
|
||||||
"total_qty": target.TotalQty,
|
"total_qty": target.TotalQty,
|
||||||
}, nil); err != nil {
|
}, nil); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty")
|
||||||
}
|
}
|
||||||
|
targetReflow[targetReflowKey{productWarehouseID: *target.ProductWarehouseId}] = struct{}{}
|
||||||
|
|
||||||
stockLogIncrease := &entity.StockLog{
|
stockLogIncrease := &entity.StockLog{
|
||||||
ProductWarehouseId: *target.ProductWarehouseId,
|
ProductWarehouseId: *target.ProductWarehouseId,
|
||||||
@@ -1376,6 +1388,11 @@ func (s *transferLayingService) executeApprovedTransferMovement(
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok masuk")
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal membuat log stok masuk")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for key := range targetReflow {
|
||||||
|
if err := reflowTransferLayingScope(ctx, s.FifoStockV2Svc, tx, key.productWarehouseID, &asOf); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal reflow FIFO v2 target transfer laying: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1595,31 +1612,19 @@ func (s *transferLayingService) hasDownstreamRecordingOnTarget(
|
|||||||
targetProjectFlockKandangID uint,
|
targetProjectFlockKandangID uint,
|
||||||
sinceDate time.Time,
|
sinceDate time.Time,
|
||||||
) (bool, time.Time, error) {
|
) (bool, time.Time, error) {
|
||||||
if targetProjectFlockKandangID == 0 {
|
targetRepo := s.LayingTransferTargetRepo
|
||||||
return false, time.Time{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
db := s.Repository.DB().WithContext(ctx)
|
|
||||||
if tx != nil {
|
if tx != nil {
|
||||||
db = tx.WithContext(ctx)
|
targetRepo = repository.NewLayingTransferTargetRepository(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
var earliest entity.Recording
|
recordDate, err := targetRepo.GetEarliestRecordingDateByTarget(ctx, targetProjectFlockKandangID, sinceDate)
|
||||||
query := db.Model(&entity.Recording{}).
|
if err != nil {
|
||||||
Where("project_flock_kandangs_id = ?", targetProjectFlockKandangID).
|
|
||||||
Where("deleted_at IS NULL")
|
|
||||||
if !sinceDate.IsZero() {
|
|
||||||
query = query.Where("record_datetime >= ?", sinceDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := query.Order("record_datetime ASC").Limit(1).Take(&earliest).Error; err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return false, time.Time{}, nil
|
|
||||||
}
|
|
||||||
return false, time.Time{}, err
|
return false, time.Time{}, err
|
||||||
}
|
}
|
||||||
|
if recordDate == nil {
|
||||||
return true, normalizeDateOnlyUTC(earliest.RecordDatetime), nil
|
return false, time.Time{}, nil
|
||||||
|
}
|
||||||
|
return true, normalizeDateOnlyUTC(*recordDate), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *transferLayingService) countActiveTransferSourceConsumeAllocations(
|
func (s *transferLayingService) countActiveTransferSourceConsumeAllocations(
|
||||||
@@ -1628,81 +1633,45 @@ func (s *transferLayingService) countActiveTransferSourceConsumeAllocations(
|
|||||||
transferID uint,
|
transferID uint,
|
||||||
productWarehouseID uint,
|
productWarehouseID uint,
|
||||||
) (int64, error) {
|
) (int64, error) {
|
||||||
if transferID == 0 || productWarehouseID == 0 {
|
targetRepo := s.LayingTransferTargetRepo
|
||||||
return 0, nil
|
if tx != nil {
|
||||||
|
targetRepo = repository.NewLayingTransferTargetRepository(tx)
|
||||||
}
|
}
|
||||||
if tx == nil {
|
return targetRepo.CountActiveTransferSourceConsumeAllocations(ctx, transferID, productWarehouseID)
|
||||||
return 0, errors.New("transaction is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
var count int64
|
|
||||||
if err := tx.WithContext(ctx).
|
|
||||||
Model(&entity.StockAllocation{}).
|
|
||||||
Where("product_warehouse_id = ?", productWarehouseID).
|
|
||||||
Where("usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()).
|
|
||||||
Where("usable_id = ?", transferID).
|
|
||||||
Where("status = ?", entity.StockAllocationStatusActive).
|
|
||||||
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
|
||||||
Count(&count).Error; err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return count, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *transferLayingService) resyncPopulationUsageByProjectFlockKandang(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
|
func (s *transferLayingService) resyncPopulationUsageByProjectFlockKandang(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
|
||||||
if projectFlockKandangID == 0 {
|
targetRepo := s.LayingTransferTargetRepo
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
db := s.Repository.DB().WithContext(ctx)
|
|
||||||
if tx != nil {
|
if tx != nil {
|
||||||
db = tx.WithContext(ctx)
|
targetRepo = repository.NewLayingTransferTargetRepository(tx)
|
||||||
}
|
}
|
||||||
|
return targetRepo.SyncPopulationUsageByProjectFlockKandang(ctx, projectFlockKandangID)
|
||||||
|
}
|
||||||
|
|
||||||
var populationIDs []uint
|
func sortedIDs(input map[uint]struct{}) []uint {
|
||||||
if err := db.Table("project_flock_populations pfp").
|
if len(input) == 0 {
|
||||||
Select("pfp.id").
|
|
||||||
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
|
||||||
Where("pc.project_flock_kandang_id = ?", projectFlockKandangID).
|
|
||||||
Pluck("pfp.id", &populationIDs).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(populationIDs) == 0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
out := make([]uint, 0, len(input))
|
||||||
type usageRow struct {
|
for id := range input {
|
||||||
StockableID uint `gorm:"column:stockable_id"`
|
if id == 0 {
|
||||||
Used float64 `gorm:"column:used"`
|
continue
|
||||||
}
|
|
||||||
var usageRows []usageRow
|
|
||||||
if err := db.Table("stock_allocations").
|
|
||||||
Select("stockable_id, COALESCE(SUM(qty), 0) AS used").
|
|
||||||
Where("stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
|
||||||
Where("status = ?", entity.StockAllocationStatusActive).
|
|
||||||
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
|
||||||
Where("stockable_id IN ?", populationIDs).
|
|
||||||
Group("stockable_id").
|
|
||||||
Scan(&usageRows).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.Model(&entity.ProjectFlockPopulation{}).
|
|
||||||
Where("id IN ?", populationIDs).
|
|
||||||
Update("total_used_qty", 0).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, row := range usageRows {
|
|
||||||
if err := db.Model(&entity.ProjectFlockPopulation{}).
|
|
||||||
Where("id = ?", row.StockableID).
|
|
||||||
Update("total_used_qty", row.Used).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
out = append(out, id)
|
||||||
}
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
func joinUint(values []uint) string {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
parts := make([]string, 0, len(values))
|
||||||
|
for _, value := range values {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d", value))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "|")
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeDateOnlyUTC(value time.Time) time.Time {
|
func normalizeDateOnlyUTC(value time.Time) time.Time {
|
||||||
@@ -1721,3 +1690,84 @@ func isLegacyTransfer(transfer *entity.LayingTransfer) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *transferLayingService) ensureNoDownstreamConsumptionForDelete(
|
||||||
|
ctx context.Context,
|
||||||
|
tx *gorm.DB,
|
||||||
|
transferNumber string,
|
||||||
|
targets []entity.LayingTransferTarget,
|
||||||
|
) error {
|
||||||
|
targetIDs := make([]uint, 0, len(targets))
|
||||||
|
for _, target := range targets {
|
||||||
|
if target.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
targetIDs = append(targetIDs, target.Id)
|
||||||
|
}
|
||||||
|
if len(targetIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
targetRepo := s.LayingTransferTargetRepo
|
||||||
|
if tx != nil {
|
||||||
|
targetRepo = repository.NewLayingTransferTargetRepository(tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := targetRepo.GetActiveDownstreamConsumptions(ctx, targetIDs)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to validate downstream consumption for transfer laying %s: %+v", transferNumber, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi transaksi turunan transfer laying")
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencyMap := make(map[string]map[uint]struct{})
|
||||||
|
for _, row := range rows {
|
||||||
|
label := mapTransferLayingDownstreamUsableLabel(row.UsableType)
|
||||||
|
if _, ok := dependencyMap[label]; !ok {
|
||||||
|
dependencyMap[label] = make(map[uint]struct{})
|
||||||
|
}
|
||||||
|
dependencyMap[label][row.UsableID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
labels := make([]string, 0, len(dependencyMap))
|
||||||
|
for label := range dependencyMap {
|
||||||
|
labels = append(labels, label)
|
||||||
|
}
|
||||||
|
sort.Strings(labels)
|
||||||
|
|
||||||
|
details := make([]string, 0, len(labels))
|
||||||
|
for _, label := range labels {
|
||||||
|
details = append(details, fmt.Sprintf("%s=%s", label, joinUint(sortedIDs(dependencyMap[label]))))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"%s Transfer %s. Dependensi aktif: %s.",
|
||||||
|
transferLayingDeleteDownstreamGuardMessage,
|
||||||
|
transferNumber,
|
||||||
|
strings.Join(details, ", "),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapTransferLayingDownstreamUsableLabel(usableType string) string {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(usableType)) {
|
||||||
|
case fifo.UsableKeyRecordingStock.String(), fifo.UsableKeyRecordingDepletion.String():
|
||||||
|
return "Recording"
|
||||||
|
case fifo.UsableKeyProjectChickin.String():
|
||||||
|
return "Chickin"
|
||||||
|
case fifo.UsableKeyMarketingDelivery.String():
|
||||||
|
return "Marketing"
|
||||||
|
case fifo.UsableKeyTransferToLayingOut.String():
|
||||||
|
return "TransferToLaying"
|
||||||
|
case fifo.UsableKeyStockTransferOut.String():
|
||||||
|
return "TransferStock"
|
||||||
|
case fifo.UsableKeyAdjustmentOut.String():
|
||||||
|
return "Adjustment"
|
||||||
|
default:
|
||||||
|
return strings.ToUpper(strings.TrimSpace(usableType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user