mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
Merge branch 'dev/teguh' into 'development'
FIX[BE]: fix transfer to laying, fix delete biaya, fix chickin, fix nominal expense, and other See merge request mbugroup/lti-api!136
This commit is contained in:
@@ -228,7 +228,13 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
|
||||
|
||||
switch {
|
||||
case delta > 0:
|
||||
allocationRes, err := s.allocateFromStock(ctx, tx, productWarehouseID, req.UsableKey, req.UsableID, delta)
|
||||
|
||||
var excludedStockables []fifo.StockableKey
|
||||
if cfg.ExcludedStockables != nil {
|
||||
excludedStockables = cfg.ExcludedStockables
|
||||
}
|
||||
|
||||
allocationRes, err := s.allocateFromStock(ctx, tx, productWarehouseID, req.UsableKey, req.UsableID, delta, excludedStockables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -410,8 +416,9 @@ func (s *fifoService) allocateFromStock(
|
||||
usableKey fifo.UsableKey,
|
||||
usableID uint,
|
||||
requestQty float64,
|
||||
excludedStockables []fifo.StockableKey,
|
||||
) (*allocationOutcome, error) {
|
||||
lots, err := s.fetchStockLots(ctx, tx, productWarehouseID)
|
||||
lots, err := s.fetchStockLots(ctx, tx, productWarehouseID, excludedStockables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -492,14 +499,24 @@ func (s *fifoService) allocateFromStock(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]stockLot, error) {
|
||||
func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWarehouseID uint, excludedStockables []fifo.StockableKey) ([]stockLot, error) {
|
||||
configs := fifo.Stockables()
|
||||
if len(configs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create exclusion set for faster lookup
|
||||
excludedSet := make(map[fifo.StockableKey]bool)
|
||||
for _, key := range excludedStockables {
|
||||
excludedSet[key] = true
|
||||
}
|
||||
|
||||
var lots []stockLot
|
||||
for key, cfg := range configs {
|
||||
// Skip excluded stockables
|
||||
if excludedSet[key] {
|
||||
continue
|
||||
}
|
||||
|
||||
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
|
||||
|
||||
@@ -616,7 +633,13 @@ func (s *fifoService) resolvePendingForWarehouse(ctx context.Context, tx *gorm.D
|
||||
continue
|
||||
}
|
||||
|
||||
outcome, err := s.allocateFromStock(ctx, tx, productWarehouseID, candidate.UsableKey, candidate.UsableID, candidate.Pending)
|
||||
// Get excluded stockables from candidate usable config
|
||||
var excludedStockables []fifo.StockableKey
|
||||
if candidate.Config.ExcludedStockables != nil {
|
||||
excludedStockables = candidate.Config.ExcludedStockables
|
||||
}
|
||||
|
||||
outcome, err := s.allocateFromStock(ctx, tx, productWarehouseID, candidate.UsableKey, candidate.UsableID, candidate.Pending, excludedStockables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Revert back to NO ACTION (RESTRICT behavior)
|
||||
ALTER TABLE expense_nonstocks DROP CONSTRAINT IF EXISTS fk_expense_nonstocks_expense_id;
|
||||
|
||||
ALTER TABLE expense_nonstocks
|
||||
ADD CONSTRAINT fk_expense_nonstocks_expense_id
|
||||
FOREIGN KEY (expense_id) REFERENCES expenses(id)
|
||||
ON DELETE NO ACTION;
|
||||
|
||||
-- Revert expense_realizations FK
|
||||
ALTER TABLE expense_realizations DROP CONSTRAINT IF EXISTS fk_expense_realizations_nonstock_id;
|
||||
|
||||
ALTER TABLE expense_realizations
|
||||
ADD CONSTRAINT fk_expense_realizations_nonstock_id
|
||||
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id)
|
||||
ON DELETE NO ACTION;
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Drop existing FK constraints
|
||||
ALTER TABLE expense_nonstocks DROP CONSTRAINT IF EXISTS fk_expense_nonstocks_expense_id;
|
||||
|
||||
-- Recreate with ON DELETE CASCADE
|
||||
ALTER TABLE expense_nonstocks
|
||||
ADD CONSTRAINT fk_expense_nonstocks_expense_id
|
||||
FOREIGN KEY (expense_id) REFERENCES expenses(id)
|
||||
ON DELETE CASCADE;
|
||||
|
||||
-- Drop and recreate expense_realizations FK
|
||||
ALTER TABLE expense_realizations DROP CONSTRAINT IF EXISTS fk_expense_realizations_nonstock_id;
|
||||
|
||||
ALTER TABLE expense_realizations
|
||||
ADD CONSTRAINT fk_expense_realizations_nonstock_id
|
||||
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id)
|
||||
ON DELETE CASCADE;
|
||||
@@ -0,0 +1,20 @@
|
||||
-- Revert back to NO ACTION (for rollback safety)
|
||||
DO $$
|
||||
BEGIN
|
||||
ALTER TABLE marketing_products DROP CONSTRAINT IF EXISTS fk_marketing_products_marketing_id;
|
||||
|
||||
ALTER TABLE marketing_products
|
||||
ADD CONSTRAINT fk_marketing_products_marketing_id
|
||||
FOREIGN KEY (marketing_id) REFERENCES marketings(id)
|
||||
ON DELETE NO ACTION;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
ALTER TABLE marketing_delivery_products DROP CONSTRAINT IF EXISTS fk_marketing_delivery_products_marketing_product_id;
|
||||
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id
|
||||
FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id)
|
||||
ON DELETE NO ACTION;
|
||||
END $$;
|
||||
@@ -0,0 +1,35 @@
|
||||
-- Ensure marketing_products FK is CASCADE (it should already be, but let's make sure)
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Drop existing FK if exists
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'fk_marketing_products_marketing_id'
|
||||
) THEN
|
||||
ALTER TABLE marketing_products DROP CONSTRAINT fk_marketing_products_marketing_id;
|
||||
END IF;
|
||||
|
||||
-- Recreate with ON DELETE CASCADE
|
||||
ALTER TABLE marketing_products
|
||||
ADD CONSTRAINT fk_marketing_products_marketing_id
|
||||
FOREIGN KEY (marketing_id) REFERENCES marketings(id)
|
||||
ON DELETE CASCADE;
|
||||
END $$;
|
||||
|
||||
-- Ensure marketing_delivery_products FK is CASCADE
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Drop existing FK if exists
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'fk_marketing_delivery_products_marketing_product_id'
|
||||
) THEN
|
||||
ALTER TABLE marketing_delivery_products DROP CONSTRAINT fk_marketing_delivery_products_marketing_product_id;
|
||||
END IF;
|
||||
|
||||
-- Recreate with ON DELETE CASCADE
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id
|
||||
FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id)
|
||||
ON DELETE CASCADE;
|
||||
END $$;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
-- Drop foreign key and column
|
||||
ALTER TABLE laying_transfers
|
||||
DROP CONSTRAINT IF EXISTS fk_laying_transfers_product_warehouse_id;
|
||||
|
||||
ALTER TABLE laying_transfers
|
||||
DROP COLUMN IF EXISTS product_warehouse_id;
|
||||
|
||||
-- Drop index
|
||||
DROP INDEX IF EXISTS idx_laying_transfers_product_warehouse_id;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
-- Add product_warehouse_id to laying_transfers for FIFO support
|
||||
ALTER TABLE laying_transfers
|
||||
ADD COLUMN product_warehouse_id BIGINT;
|
||||
|
||||
-- Add foreign key
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||
ALTER TABLE laying_transfers
|
||||
ADD CONSTRAINT fk_laying_transfers_product_warehouse_id
|
||||
FOREIGN KEY (product_warehouse_id)
|
||||
REFERENCES product_warehouses(id)
|
||||
ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add index
|
||||
CREATE INDEX idx_laying_transfers_product_warehouse_id
|
||||
ON laying_transfers(product_warehouse_id);
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
-- Rollback: Remove STOCKABLE fields from laying_transfers
|
||||
|
||||
-- Drop index
|
||||
DROP INDEX IF EXISTS idx_laying_transfers_dest_product_warehouse_id;
|
||||
|
||||
-- Drop foreign key constraint
|
||||
ALTER TABLE laying_transfers
|
||||
DROP CONSTRAINT IF EXISTS fk_laying_transfers_dest_product_warehouse_id;
|
||||
|
||||
-- Drop columns
|
||||
ALTER TABLE laying_transfers
|
||||
DROP COLUMN IF EXISTS dest_product_warehouse_id,
|
||||
DROP COLUMN IF EXISTS total_qty,
|
||||
DROP COLUMN IF EXISTS total_used;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
-- Add STOCKABLE fields to laying_transfers for destination warehouse
|
||||
-- This enables Transfer to Laying to work as DUAL ROLE (Stockable + Usable)
|
||||
|
||||
-- Add columns for STOCKABLE role (destination warehouse)
|
||||
ALTER TABLE laying_transfers
|
||||
ADD COLUMN dest_product_warehouse_id BIGINT,
|
||||
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0,
|
||||
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0;
|
||||
|
||||
-- Add foreign key constraint
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||
ALTER TABLE laying_transfers
|
||||
ADD CONSTRAINT fk_laying_transfers_dest_product_warehouse_id
|
||||
FOREIGN KEY (dest_product_warehouse_id)
|
||||
REFERENCES product_warehouses(id)
|
||||
ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Add index for performance
|
||||
CREATE INDEX idx_laying_transfers_dest_product_warehouse_id
|
||||
ON laying_transfers(dest_product_warehouse_id);
|
||||
|
||||
-- Add comment for documentation
|
||||
COMMENT ON COLUMN laying_transfers.product_warehouse_id IS 'Product warehouse at source (Growing flock) - for USABLE role';
|
||||
COMMENT ON COLUMN laying_transfers.dest_product_warehouse_id IS 'Product warehouse at destination (Laying flock) - for STOCKABLE role';
|
||||
COMMENT ON COLUMN laying_transfers.total_qty IS 'Total lot quantity introduced to destination warehouse - for STOCKABLE role';
|
||||
COMMENT ON COLUMN laying_transfers.total_used IS 'Quantity already consumed from this lot at destination - for STOCKABLE role';
|
||||
@@ -12,18 +12,29 @@ type LayingTransfer struct {
|
||||
FromProjectFlockId uint `gorm:"not null"`
|
||||
ToProjectFlockId uint `gorm:"not null"`
|
||||
TransferDate time.Time `gorm:"type:date;not null"`
|
||||
|
||||
|
||||
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
|
||||
UsageQty *float64 `gorm:"type:numeric(15,3)"`
|
||||
ProductWarehouseId *uint `gorm:"type:bigint"` // Source PW (PULLET)
|
||||
|
||||
|
||||
DestProductWarehouseID *uint `gorm:"column:dest_product_warehouse_id;type:bigint"` // Destination PW (LAYER)
|
||||
TotalQty float64 `gorm:"column:total_qty;type:numeric(15,3);default:0"` // Total lot introduced to destination
|
||||
TotalUsed float64 `gorm:"column:total_used;type:numeric(15,3);default:0"` // Already consumed from this lot
|
||||
|
||||
Notes string `gorm:"type:text"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
|
||||
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
|
||||
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
|
||||
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
|
||||
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` // Source PW
|
||||
DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID;references:Id"` // Destination PW
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ type ProjectFlockKandang struct {
|
||||
ClosedAt *time.Time `gorm:"index"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
|
||||
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||
LatestProjectFlockApproval *Approval `gorm:"-" json:"-"`
|
||||
LatestChickinApproval *Approval `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
|
||||
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
@@ -32,6 +33,7 @@ type ExpenseBaseDTO struct {
|
||||
|
||||
type ExpenseListDTO struct {
|
||||
ExpenseBaseDTO
|
||||
GrandTotal float64 `json:"grand_total"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@@ -140,8 +142,11 @@ func ToExpenseListDTO(e entity.Expense) ExpenseListDTO {
|
||||
latestApproval = &mapped
|
||||
}
|
||||
|
||||
grandTotal := calculateGrandTotal(&e)
|
||||
|
||||
return ExpenseListDTO{
|
||||
ExpenseBaseDTO: ToExpenseBaseDTO(&e),
|
||||
GrandTotal: grandTotal,
|
||||
CreatedUser: createdUser,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
@@ -344,3 +349,25 @@ func ToKandangGroupDTO(pengajuans []ExpenseNonstockDTO, realisasi []ExpenseReali
|
||||
|
||||
return kandangs
|
||||
}
|
||||
|
||||
func calculateGrandTotal(expense *entity.Expense) float64 {
|
||||
|
||||
useRealization := expense.LatestApproval != nil && expense.LatestApproval.StepNumber >= uint16(utils.ExpenseStepRealisasi)
|
||||
|
||||
if useRealization {
|
||||
|
||||
var total float64
|
||||
for _, ns := range expense.Nonstocks {
|
||||
if ns.Realization != nil {
|
||||
total += ns.Realization.Qty * ns.Realization.Price
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
var total float64
|
||||
for _, ns := range expense.Nonstocks {
|
||||
total += ns.Qty * ns.Price
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
@@ -211,6 +211,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
||||
if !hasKandang && req.Category == string(utils.ExpenseCategoryBOP) {
|
||||
projectFlockRepoTx := projectFlockKandangRepo.NewProjectflockRepository(dbTransaction)
|
||||
activeProjectFlocks, err := projectFlockRepoTx.GetActiveByLocationID(c.Context(), req.LocationID)
|
||||
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get active project flocks for location")
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
|
||||
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||
|
||||
err := fifoService.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKey("ADJUSTMENT_IN"),
|
||||
Key: fifo.StockableKeyAdjustmentIn,
|
||||
Table: "adjustment_stocks",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
@@ -52,7 +52,7 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
|
||||
}
|
||||
|
||||
err = fifoService.RegisterUsable(fifo.UsableConfig{
|
||||
Key: fifo.UsableKey("ADJUSTMENT_OUT"),
|
||||
Key: fifo.UsableKeyAdjustmentOut,
|
||||
Table: "adjustment_stocks",
|
||||
Columns: fifo.UsableColumns{
|
||||
ID: "id",
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
stockLogsRepo "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/fifo"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -123,15 +124,9 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
projectFlockKandangID = &pfkID
|
||||
}
|
||||
|
||||
pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(
|
||||
ctx,
|
||||
uint(req.ProductID),
|
||||
uint(req.WarehouseID),
|
||||
projectFlockKandangID,
|
||||
)
|
||||
pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(ctx, uint(req.ProductID), uint(req.WarehouseID), projectFlockKandangID)
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to find product warehouse: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||
}
|
||||
|
||||
@@ -143,7 +138,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
}
|
||||
|
||||
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create product warehouse: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse")
|
||||
}
|
||||
pw = newPW
|
||||
@@ -163,7 +157,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||
}
|
||||
|
||||
// Create StockLog for history tracking
|
||||
afterQuantity := productWarehouse.Quantity
|
||||
newLog := &entity.StockLog{
|
||||
LoggableType: string(utils.StockLogTypeAdjustment),
|
||||
@@ -189,7 +182,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
return err
|
||||
}
|
||||
|
||||
// Create AdjustmentStock record for FIFO tracking
|
||||
adjustmentStock := &entity.AdjustmentStock{
|
||||
StockLogId: newLog.Id,
|
||||
ProductWarehouseId: productWarehouse.Id,
|
||||
@@ -200,10 +192,10 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
}
|
||||
|
||||
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
||||
// Adjustment INCREASE → Replenish stock (Stockable)
|
||||
|
||||
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
|
||||
_, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
|
||||
StockableKey: "ADJUSTMENT_IN",
|
||||
StockableKey: fifo.StockableKeyAdjustmentIn,
|
||||
StockableID: adjustmentStock.Id,
|
||||
ProductWarehouseID: uint(productWarehouse.Id),
|
||||
Quantity: req.Quantity,
|
||||
@@ -215,9 +207,8 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
}
|
||||
|
||||
} else {
|
||||
// Adjustment DECREASE → Consume stock (Usable)
|
||||
_, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
|
||||
UsableKey: "ADJUSTMENT_OUT",
|
||||
UsableKey: fifo.UsableKeyAdjustmentOut,
|
||||
UsableID: adjustmentStock.Id,
|
||||
ProductWarehouseID: uint(productWarehouse.Id),
|
||||
Quantity: req.Quantity,
|
||||
@@ -230,6 +221,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
}
|
||||
|
||||
// Update ProductWarehouse quantity (for backward compatibility/reporting)
|
||||
|
||||
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)
|
||||
|
||||
@@ -235,6 +235,15 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
||||
|
||||
var deliveries []TransferDeliveryDTO
|
||||
for _, del := range e.Deliveries {
|
||||
var items []TransferDeliveryItemDTO
|
||||
for _, item := range del.Items {
|
||||
items = append(items, TransferDeliveryItemDTO{
|
||||
Id: item.Id,
|
||||
StockTransferDetailId: item.StockTransferDetailId,
|
||||
Quantity: item.Quantity,
|
||||
})
|
||||
}
|
||||
|
||||
var document *DocumentDTO
|
||||
if len(del.Documents) > 0 {
|
||||
doc := del.Documents[0] // Take first document
|
||||
@@ -258,6 +267,7 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
||||
DocumentNumber: del.DocumentNumber,
|
||||
ShippingCostItem: del.ShippingCostItem,
|
||||
ShippingCostTotal: del.ShippingCostTotal,
|
||||
Items: items,
|
||||
Document: document,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -43,12 +43,10 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Initialize FIFO Service
|
||||
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||
|
||||
// Register Transfer as Stockable (adds stock to destination warehouse)
|
||||
err = fifoService.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKey("STOCK_TRANSFER_IN"),
|
||||
Key: fifo.StockableKeyStockTransferIn,
|
||||
Table: "stock_transfer_details",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
@@ -63,9 +61,8 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Register Transfer as Usable (consumes stock from source warehouse)
|
||||
err = fifoService.RegisterUsable(fifo.UsableConfig{
|
||||
Key: fifo.UsableKey("STOCK_TRANSFER_OUT"),
|
||||
Key: fifo.UsableKeyStockTransferOut,
|
||||
Table: "stock_transfer_details",
|
||||
Columns: fifo.UsableColumns{
|
||||
ID: "id",
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/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/fifo"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -337,24 +338,21 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
}
|
||||
|
||||
// Execute FIFO operations for each product
|
||||
for _, product := range req.Products {
|
||||
detail := detailMap[uint64(product.ProductID)]
|
||||
|
||||
// Step 1: Consume stock from source warehouse (STOCK_TRANSFER_OUT)
|
||||
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
|
||||
UsableKey: "STOCK_TRANSFER_OUT",
|
||||
UsableKey: fifo.UsableKeyStockTransferOut,
|
||||
UsableID: uint(detail.Id),
|
||||
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
|
||||
Quantity: product.ProductQty,
|
||||
AllowPending: false, // Don't allow pending, must have actual stock
|
||||
AllowPending: false,
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak cukup di gudang asal untuk produk %d: %v", product.ProductID, err))
|
||||
}
|
||||
|
||||
// Update usage tracking fields for source warehouse
|
||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||
Where("id = ?", detail.Id).
|
||||
Updates(map[string]interface{}{
|
||||
@@ -367,7 +365,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
// Step 2: Replenish stock to destination warehouse (STOCK_TRANSFER_IN)
|
||||
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
||||
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||
StockableKey: "STOCK_TRANSFER_IN",
|
||||
StockableKey: fifo.StockableKeyStockTransferIn,
|
||||
StockableID: uint(detail.Id),
|
||||
ProductWarehouseID: uint(*detail.DestProductWarehouseID),
|
||||
Quantity: product.ProductQty,
|
||||
@@ -378,7 +376,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal menambah stok di gudang tujuan untuk produk %d: %v", product.ProductID, err))
|
||||
}
|
||||
|
||||
// Update total tracking fields for destination warehouse
|
||||
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||
Where("id = ?", detail.Id).
|
||||
Updates(map[string]interface{}{
|
||||
|
||||
@@ -52,12 +52,31 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
||||
PendingQuantity: "pending_usage_qty",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
|
||||
ExcludedStockables: []fifo.StockableKey{fifo.StockableKeyProjectFlockPopulation},
|
||||
}); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
panic(fmt.Sprintf("failed to register chickin usable workflow: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := fifoService.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKeyProjectFlockPopulation,
|
||||
Table: "project_flock_populations",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
TotalQuantity: "total_qty",
|
||||
TotalUsedQuantity: "total_used_qty",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
OrderBy: []string{"created_at ASC", "id ASC"},
|
||||
}); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
panic(fmt.Sprintf("failed to register project flock population stockable workflow: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil {
|
||||
|
||||
@@ -16,6 +16,8 @@ type ProjectChickinRepository interface {
|
||||
GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
||||
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||
GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error)
|
||||
GetByProjectFlockKandangIDForUpdate(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
||||
UpdateUsageFields(ctx context.Context, tx *gorm.DB, chickinID uint, usageQty, pendingUsageQty float64) error
|
||||
}
|
||||
|
||||
type ChickinRepositoryImpl struct {
|
||||
@@ -64,6 +66,26 @@ func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context,
|
||||
return chickins, nil
|
||||
}
|
||||
|
||||
// GetByProjectFlockKandangIDForUpdate locks chickin rows to prevent race condition
|
||||
func (r *ChickinRepositoryImpl) GetByProjectFlockKandangIDForUpdate(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
|
||||
var chickins []entity.ProjectChickin
|
||||
// CRITICAL: Use FOR UPDATE to lock rows and prevent concurrent chickin requests
|
||||
// This ensures that simultaneous requests wait for each other and read consistent pending_qty
|
||||
err := r.db.WithContext(ctx).
|
||||
Raw(`
|
||||
SELECT * FROM project_chickins
|
||||
WHERE project_flock_kandang_id = ?
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
FOR UPDATE
|
||||
`, projectFlockKandangID).
|
||||
Scan(&chickins).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return chickins, nil
|
||||
}
|
||||
|
||||
func (r *ChickinRepositoryImpl) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
|
||||
var chickins []entity.ProjectChickin
|
||||
err := r.db.WithContext(ctx).
|
||||
@@ -102,3 +124,13 @@ func (r *ChickinRepositoryImpl) GetTotalChickinQtyByProjectFlockID(ctx context.C
|
||||
Scan(&result).Error
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (r *ChickinRepositoryImpl) UpdateUsageFields(ctx context.Context, tx *gorm.DB, chickinID uint, usageQty, pendingUsageQty float64) error {
|
||||
return tx.WithContext(ctx).
|
||||
Model(&entity.ProjectChickin{}).
|
||||
Where("id = ?", chickinID).
|
||||
Updates(map[string]interface{}{
|
||||
"usage_qty": usageQty,
|
||||
"pending_usage_qty": pendingUsageQty,
|
||||
}).Error
|
||||
}
|
||||
|
||||
@@ -112,7 +112,6 @@ func (s chickinService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectChickin, e
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed get chickin by id: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
return chickin, nil
|
||||
@@ -137,12 +136,15 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newChikins := make([]*entity.ProjectChickin, 0)
|
||||
chickinQtyMap := make(map[uint]float64)
|
||||
|
||||
for idx, chickinReq := range req.ChickinRequests {
|
||||
|
||||
productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickinReq.ProductWarehouseId, nil)
|
||||
productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickinReq.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Product.Flags")
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickinReq.ProductWarehouseId))
|
||||
}
|
||||
@@ -151,8 +153,32 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not bound to kandang's warehouse", chickinReq.ProductWarehouseId))
|
||||
}
|
||||
|
||||
if productWarehouse.ProjectFlockKandangId == nil || *productWarehouse.ProjectFlockKandangId != req.ProjectFlockKandangId {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not attached to project_flock_kandang %d. Only product warehouses with matching project_flock_kandang_id can be chickin-ed", chickinReq.ProductWarehouseId, req.ProjectFlockKandangId))
|
||||
if productWarehouse.ProjectFlockKandangId != nil && *productWarehouse.ProjectFlockKandangId != req.ProjectFlockKandangId {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d belongs to different flock. Only product warehouses with project_flock_kandang_id = NULL or = %d can be used", chickinReq.ProductWarehouseId, req.ProjectFlockKandangId))
|
||||
}
|
||||
|
||||
if productWarehouse.Product.Id != 0 {
|
||||
|
||||
var requiredFlag utils.FlagType
|
||||
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
|
||||
requiredFlag = utils.FlagDOC
|
||||
} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||
requiredFlag = utils.FlagPullet
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid flock category for chickin")
|
||||
}
|
||||
|
||||
hasRequiredFlag := false
|
||||
for _, flag := range productWarehouse.Product.Flags {
|
||||
if utils.FlagType(flag.Name) == requiredFlag {
|
||||
hasRequiredFlag = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasRequiredFlag {
|
||||
return nil, fmt.Errorf("product warehouse %d cannot be used for %s chickin. Product must have %s flag (product ID: %d, warehouse ID: %d)", chickinReq.ProductWarehouseId, projectFlockKandang.ProjectFlock.Category, requiredFlag, productWarehouse.Product.Id, productWarehouse.Id)
|
||||
}
|
||||
}
|
||||
|
||||
chickinDate, err := utils.ParseDateString(chickinReq.ChickInDate)
|
||||
@@ -160,11 +186,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId))
|
||||
}
|
||||
|
||||
availableQty := productWarehouse.Quantity
|
||||
if availableQty <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No available stock in product warehouse %d for chickin", chickinReq.ProductWarehouseId))
|
||||
}
|
||||
|
||||
newChickin := &entity.ProjectChickin{
|
||||
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
||||
ChickInDate: chickinDate,
|
||||
@@ -176,6 +197,17 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
}
|
||||
|
||||
newChikins = append(newChikins, newChickin)
|
||||
|
||||
totalPopulationQty, err := s.ProjectflockPopulationRepo.GetTotalQtyByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get total population quantity for project_flock_kandang %d", req.ProjectFlockKandangId))
|
||||
}
|
||||
|
||||
availableQty := productWarehouse.Quantity - totalPopulationQty
|
||||
if availableQty < 0 {
|
||||
availableQty = 0
|
||||
}
|
||||
|
||||
chickinQtyMap[uint(idx)] = availableQty
|
||||
}
|
||||
|
||||
@@ -183,15 +215,40 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "No chickins to create")
|
||||
}
|
||||
|
||||
existingChikins, err := s.Repository.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing chickins")
|
||||
}
|
||||
|
||||
isFirstTime := len(existingChikins) == 0
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
|
||||
repositoryTx := repository.NewChickinRepository(dbTransaction)
|
||||
existingChikins, err := repositoryTx.GetByProjectFlockKandangIDForUpdate(c.Context(), req.ProjectFlockKandangId)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing chickins")
|
||||
}
|
||||
|
||||
isFirstTime := len(existingChikins) == 0
|
||||
|
||||
pendingQtyMap := make(map[uint]float64)
|
||||
for _, existingChickin := range existingChikins {
|
||||
if existingChickin.PendingUsageQty > 0 {
|
||||
pendingQtyMap[existingChickin.ProductWarehouseId] += existingChickin.PendingUsageQty
|
||||
}
|
||||
}
|
||||
for idx, chickin := range newChikins {
|
||||
pendingQty := pendingQtyMap[chickin.ProductWarehouseId]
|
||||
desiredQty := chickinQtyMap[uint(idx)]
|
||||
|
||||
availableQty := desiredQty - pendingQty
|
||||
if availableQty < 0 {
|
||||
availableQty = 0
|
||||
}
|
||||
|
||||
if availableQty <= 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No available stock in product warehouse %d for chickin. Warehouse: %.0f, Pending: %.0f, Available: %.0f", chickin.ProductWarehouseId, desiredQty, pendingQty, availableQty))
|
||||
}
|
||||
|
||||
chickinQtyMap[uint(idx)] = availableQty
|
||||
|
||||
pendingQtyMap[chickin.ProductWarehouseId] += availableQty
|
||||
}
|
||||
|
||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
|
||||
if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil {
|
||||
@@ -313,12 +370,15 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
}
|
||||
|
||||
if chickin.UsageQty > 0 {
|
||||
|
||||
currentUsageQty := chickin.UsageQty
|
||||
|
||||
if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
warehouseDeltas := make(map[uint]float64)
|
||||
warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty
|
||||
warehouseDeltas[chickin.ProductWarehouseId] += currentUsageQty
|
||||
if err := s.adjustProductWarehouseQuantities(c.Context(), s.Repository.DB(), warehouseDeltas); err != nil {
|
||||
s.Log.Errorf("Failed to adjust product warehouses for deleted chickin %d: %+v", chickin.Id, err)
|
||||
return err
|
||||
@@ -552,6 +612,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
||||
}
|
||||
|
||||
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
|
||||
chickinRepoTx := s.Repository.WithTx(dbTransaction)
|
||||
|
||||
var totalQuantityAdded float64
|
||||
|
||||
@@ -572,7 +633,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
||||
population := &entity.ProjectFlockPopulation{
|
||||
ProjectChickinId: chickin.Id,
|
||||
ProductWarehouseId: targetPW.Id,
|
||||
TotalQty: quantityToConvert,
|
||||
TotalQty: 0, // Will be set by FIFO Replenish
|
||||
TotalUsedQty: 0,
|
||||
Notes: chickin.Notes,
|
||||
CreatedBy: actorID,
|
||||
@@ -581,18 +642,29 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
||||
return err
|
||||
}
|
||||
|
||||
// Reset PendingUsageQty to 0 since population has been created
|
||||
if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{
|
||||
"pending_usage_qty": 0,
|
||||
}, nil); err != nil {
|
||||
return fmt.Errorf("failed to reset pending usage qty for chickin %d: %w", chickin.Id, err)
|
||||
}
|
||||
|
||||
// Replenish stock to target ProductWarehouse based on source flag
|
||||
// StockableKey is PROJECT_CHICKIN but StockableID refers to Population ID
|
||||
if err := s.ReplenishChickinStocks(ctx.Context(), dbTransaction, &chickin, targetPW, population, actorID); err != nil {
|
||||
s.Log.Errorf("Failed to replenish stock for chickin %d: %+v", chickin.Id, err)
|
||||
return err
|
||||
}
|
||||
|
||||
totalQuantityAdded += quantityToConvert
|
||||
}
|
||||
|
||||
if totalQuantityAdded > 0 {
|
||||
if err := s.ProductWarehouseRepo.AdjustQuantities(ctx.Context(), map[uint]float64{
|
||||
targetPW.Id: totalQuantityAdded,
|
||||
}, func(db *gorm.DB) *gorm.DB {
|
||||
return dbTransaction
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to update target product warehouse quantity: %w", err)
|
||||
}
|
||||
}
|
||||
// NOTE: ProductWarehouse target sudah ditambah melalui ReplenishChickinStocks
|
||||
// yang dipanggil di atas untuk setiap chickin berdasarkan flag source:
|
||||
// - DOC → replenish ke PULLET
|
||||
// - PULLET → replenish ke LAYER
|
||||
// - LAYER → tidak perlu replenish (sudah final)
|
||||
// - DOC+PULLET+LAYER → replenish ke dirinya sendiri
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -621,10 +693,7 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
|
||||
s.Log.Infof("ConsumeChickinStocks result: usage_qty=%.3f, pending_qty=%.3f, allocated_allocations=%d",
|
||||
result.UsageQuantity, result.PendingQuantity, len(result.AddedAllocations))
|
||||
|
||||
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Updates(map[string]interface{}{
|
||||
"usage_qty": result.UsageQuantity,
|
||||
"pending_usage_qty": result.PendingQuantity,
|
||||
}).Error; err != nil {
|
||||
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -646,6 +715,101 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, targetPW *entity.ProductWarehouse, population *entity.ProjectFlockPopulation, actorID uint) error {
|
||||
if chickin == nil || targetPW == nil || population == nil || s.FifoSvc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
sourcePW, err := s.ProductWarehouseRepo.GetByID(ctx, chickin.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Product.Flags")
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
return err
|
||||
}
|
||||
if sourcePW == nil || sourcePW.Product.Id == 0 {
|
||||
return fmt.Errorf("source product warehouse or product not found for chickin %d", chickin.Id)
|
||||
}
|
||||
|
||||
sourceFlags := sourcePW.Product.Flags
|
||||
if len(sourceFlags) == 0 {
|
||||
s.Log.Warnf("Source product %d has no flags, skipping replenish for chickin %d", sourcePW.Product.Id, chickin.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
hasDoc := false
|
||||
hasPullet := false
|
||||
hasLayer := false
|
||||
for _, flag := range sourceFlags {
|
||||
flagName := utils.FlagType(flag.Name)
|
||||
if flagName == utils.FlagDOC {
|
||||
hasDoc = true
|
||||
} else if flagName == utils.FlagPullet {
|
||||
hasPullet = true
|
||||
} else if flagName == utils.FlagLayer {
|
||||
hasLayer = true
|
||||
}
|
||||
}
|
||||
|
||||
if hasDoc && hasPullet && hasLayer {
|
||||
s.Log.Infof("Chickin %d has mixed flags (DOC+PULLET+LAYER), replenishing to source PW %d", chickin.Id, sourcePW.Id)
|
||||
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
||||
StockableID: population.Id,
|
||||
ProductWarehouseID: sourcePW.Id,
|
||||
Quantity: chickin.UsageQty,
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to replenish stock to source PW for chickin %d: %+v", chickin.Id, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LAYER only - no replenish needed
|
||||
if hasLayer && !hasDoc && !hasPullet {
|
||||
s.Log.Infof("Chickin %d has LAYER flag only, skipping replenish", chickin.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
if hasDoc && !hasPullet && !hasLayer {
|
||||
s.Log.Infof("Chickin %d has DOC flag, replenishing to PULLET PW %d", chickin.Id, targetPW.Id)
|
||||
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
||||
StockableID: population.Id,
|
||||
ProductWarehouseID: targetPW.Id,
|
||||
Quantity: chickin.UsageQty,
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to replenish stock to PULLET PW for chickin %d: %+v", chickin.Id, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if hasPullet && !hasDoc && !hasLayer {
|
||||
s.Log.Infof("Chickin %d has PULLET flag, replenishing to LAYER PW %d", chickin.Id, targetPW.Id)
|
||||
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
||||
StockableID: population.Id,
|
||||
ProductWarehouseID: targetPW.Id,
|
||||
Quantity: chickin.UsageQty,
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to replenish stock to LAYER PW for chickin %d: %+v", chickin.Id, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Other combinations (e.g., DOC + PULLET without LAYER) - skip for now
|
||||
s.Log.Warnf("Chickin %d has unsupported flag combination, skipping replenish", chickin.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, actorID uint) error {
|
||||
if chickin == nil || s.FifoSvc == nil {
|
||||
return nil
|
||||
@@ -653,8 +817,7 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
||||
|
||||
var currentUsage float64
|
||||
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(¤tUsage).Error; err != nil {
|
||||
s.Log.Warnf("Failed to get current usage for chickin %d: %+v", chickin.Id, err)
|
||||
currentUsage = 0
|
||||
|
||||
}
|
||||
|
||||
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
|
||||
@@ -666,14 +829,10 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Updates(map[string]interface{}{
|
||||
"usage_qty": 0,
|
||||
"pending_usage_qty": 0,
|
||||
}).Error; err != nil {
|
||||
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create stock log for the restoration
|
||||
if currentUsage > 0 {
|
||||
increaseLog := &entity.StockLog{
|
||||
Increase: currentUsage,
|
||||
@@ -684,8 +843,7 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
||||
Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id),
|
||||
}
|
||||
if err := s.StockLogRepo.CreateOne(ctx, increaseLog, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock log for chickin %d: %+v", chickin.Id, err)
|
||||
// Don't return error here, stock already released
|
||||
s.Log.Errorf("Failed to create stock log for released chickin %d: %+v", chickin.Id, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+27
-27
@@ -28,14 +28,14 @@ type ProjectFlockKandangRelationDTO struct {
|
||||
|
||||
type ProjectFlockDTO struct {
|
||||
projectFlockDTO.ProjectFlockRelationDTO
|
||||
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
|
||||
Category string `json:"category"`
|
||||
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"`
|
||||
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
|
||||
Category string `json:"category"`
|
||||
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"`
|
||||
ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"`
|
||||
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ProductWarehouseDTO struct {
|
||||
@@ -51,11 +51,12 @@ type AvailableQtyDTO struct {
|
||||
|
||||
type ProjectFlockKandangListDTO struct {
|
||||
ProjectFlockKandangRelationDTO
|
||||
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
|
||||
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
|
||||
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
|
||||
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
|
||||
ChickinApproval *approvalDTO.ApprovalRelationDTO `json:"chickin_approval,omitempty"`
|
||||
}
|
||||
|
||||
type ProjectFlockKandangDetailDTO struct {
|
||||
@@ -105,7 +106,8 @@ func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang
|
||||
Kandang: toKandangRelation(e.Kandang),
|
||||
CreatedAt: e.CreatedAt,
|
||||
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
||||
Approval: toApprovalDTO(e),
|
||||
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
|
||||
ChickinApproval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestChickinApproval }),
|
||||
}
|
||||
|
||||
return ProjectFlockKandangDetailDTO{
|
||||
@@ -124,9 +126,11 @@ func toKandangRelation(kandang entity.Kandang) *kandangDTO.KandangRelationDTO {
|
||||
return &mapped
|
||||
}
|
||||
|
||||
func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalRelationDTO {
|
||||
if e.LatestApproval != nil {
|
||||
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
||||
func toApprovalDTOSelector(
|
||||
e entity.ProjectFlockKandang, selector func(entity.ProjectFlockKandang) *entity.Approval) *approvalDTO.ApprovalRelationDTO {
|
||||
approval := selector(e)
|
||||
if approval != nil {
|
||||
mapped := approvalDTO.ToApprovalDTO(*approval)
|
||||
return &mapped
|
||||
}
|
||||
return nil
|
||||
@@ -145,18 +149,11 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand
|
||||
Kandang: toKandangRelation(e.Kandang),
|
||||
CreatedAt: e.CreatedAt,
|
||||
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
||||
Approval: toApprovalDTO(e),
|
||||
Approval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestProjectFlockApproval }),
|
||||
ChickinApproval: toApprovalDTOSelector(e, func(x entity.ProjectFlockKandang) *entity.Approval { return x.LatestChickinApproval }),
|
||||
}
|
||||
}
|
||||
|
||||
func ToProjectFlockKandangListDTOs(e []entity.ProjectFlockKandang) []ProjectFlockKandangListDTO {
|
||||
result := make([]ProjectFlockKandangListDTO, len(e))
|
||||
for i, r := range e {
|
||||
result[i] = ToProjectFlockKandangListDTO(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func toCreatedUserDTO(pf entity.ProjectFlock) *userDTO.UserRelationDTO {
|
||||
if pf.CreatedUser.Id != 0 {
|
||||
mapped := userDTO.ToUserRelationDTO(pf.CreatedUser)
|
||||
@@ -187,7 +184,6 @@ func toAvailableQtyDTOsFromMap(chickins []entity.ProjectChickin, availableQtyMap
|
||||
return nil
|
||||
}
|
||||
|
||||
// First, build map from chickins
|
||||
pwMap := make(map[uint]*entity.ProductWarehouse)
|
||||
for _, chickin := range chickins {
|
||||
if chickin.ProductWarehouse != nil && chickin.ProductWarehouse.Id != 0 {
|
||||
@@ -195,7 +191,6 @@ func toAvailableQtyDTOsFromMap(chickins []entity.ProjectChickin, availableQtyMap
|
||||
}
|
||||
}
|
||||
|
||||
// Then, add productWarehouses that are not in chickins yet
|
||||
for i := range productWarehouses {
|
||||
if _, exists := pwMap[productWarehouses[i].Id]; !exists {
|
||||
pwMap[productWarehouses[i].Id] = &productWarehouses[i]
|
||||
@@ -204,6 +199,11 @@ func toAvailableQtyDTOsFromMap(chickins []entity.ProjectChickin, availableQtyMap
|
||||
|
||||
result := make([]AvailableQtyDTO, 0, len(availableQtyMap))
|
||||
for pwId, availableQty := range availableQtyMap {
|
||||
|
||||
if availableQty <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
pw, exists := pwMap[pwId]
|
||||
if !exists || pw == nil {
|
||||
continue
|
||||
|
||||
+90
-28
@@ -98,23 +98,8 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer
|
||||
}
|
||||
|
||||
if s.ApprovalSvc != nil {
|
||||
projectFlockKandangIDs := make([]uint, len(projectFlockKandangs))
|
||||
for i, pfk := range projectFlockKandangs {
|
||||
projectFlockKandangIDs[i] = pfk.Id
|
||||
}
|
||||
|
||||
approvalMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandangIDs, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("ActionUser")
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Warnf("Failed to fetch approvals for projectFlockKandangs: %+v", err)
|
||||
} else {
|
||||
for i := range projectFlockKandangs {
|
||||
if approval, ok := approvalMap[projectFlockKandangs[i].Id]; ok {
|
||||
projectFlockKandangs[i].LatestApproval = approval
|
||||
}
|
||||
}
|
||||
}
|
||||
s.fetchProjectFlockApprovals(c, projectFlockKandangs)
|
||||
s.fetchChickinApprovals(c, projectFlockKandangs)
|
||||
}
|
||||
|
||||
return projectFlockKandangs, total, nil
|
||||
@@ -130,14 +115,8 @@ func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Proje
|
||||
}
|
||||
|
||||
if len(projectFlockKandang.Chickins) > 0 && s.ApprovalSvc != nil {
|
||||
latest, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch latest kandang approval for projectFlockKandang %d: %+v", projectFlockKandang.Id, err)
|
||||
}
|
||||
|
||||
if latest != nil {
|
||||
projectFlockKandang.LatestApproval = latest
|
||||
}
|
||||
s.fetchProjectFlockApproval(c, projectFlockKandang)
|
||||
s.fetchChickinApproval(c, projectFlockKandang)
|
||||
}
|
||||
|
||||
availableQtyMap, err := s.getAvailableQuantities(c, projectFlockKandang)
|
||||
@@ -164,6 +143,68 @@ func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Proje
|
||||
return projectFlockKandang, availableQtyMap, productWarehouses, nil
|
||||
}
|
||||
|
||||
func (s projectFlockKandangService) fetchProjectFlockApprovals(c *fiber.Ctx, projectFlockKandangs []entity.ProjectFlockKandang) {
|
||||
projectFlockKandangIDs := make([]uint, len(projectFlockKandangs))
|
||||
for i, pfk := range projectFlockKandangs {
|
||||
projectFlockKandangIDs[i] = pfk.Id
|
||||
}
|
||||
|
||||
approvalMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandangIDs, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("ActionUser")
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Warnf("Failed to fetch approvals for projectFlockKandangs: %+v", err)
|
||||
} else {
|
||||
for i := range projectFlockKandangs {
|
||||
if approval, ok := approvalMap[projectFlockKandangs[i].Id]; ok {
|
||||
projectFlockKandangs[i].LatestProjectFlockApproval = approval
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s projectFlockKandangService) fetchChickinApprovals(c *fiber.Ctx, projectFlockKandangs []entity.ProjectFlockKandang) {
|
||||
projectFlockKandangIDs := make([]uint, len(projectFlockKandangs))
|
||||
for i, pfk := range projectFlockKandangs {
|
||||
projectFlockKandangIDs[i] = pfk.Id
|
||||
}
|
||||
|
||||
chickinApprovalMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowChickin, projectFlockKandangIDs, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("ActionUser")
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Warnf("Failed to fetch chickin approvals for projectFlockKandangs: %+v", err)
|
||||
} else {
|
||||
for i := range projectFlockKandangs {
|
||||
if approval, ok := chickinApprovalMap[projectFlockKandangs[i].Id]; ok {
|
||||
projectFlockKandangs[i].LatestChickinApproval = approval
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s projectFlockKandangService) fetchProjectFlockApproval(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) {
|
||||
latest, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch latest kandang approval for projectFlockKandang %d: %+v", projectFlockKandang.Id, err)
|
||||
}
|
||||
|
||||
if latest != nil {
|
||||
projectFlockKandang.LatestProjectFlockApproval = latest
|
||||
}
|
||||
}
|
||||
|
||||
func (s projectFlockKandangService) fetchChickinApproval(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) {
|
||||
latestChickin, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, projectFlockKandang.Id, nil)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch latest chickin approval for projectFlockKandang %d: %+v", projectFlockKandang.Id, err)
|
||||
}
|
||||
|
||||
if latestChickin != nil {
|
||||
projectFlockKandang.LatestChickinApproval = latestChickin
|
||||
}
|
||||
}
|
||||
|
||||
func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) (map[uint]float64, error) {
|
||||
if projectFlockKandang.Kandang.Id == 0 || s.WarehouseRepo == nil || s.ProductWarehouseRepo == nil {
|
||||
return nil, nil
|
||||
@@ -191,7 +232,7 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project
|
||||
result := make(map[uint]float64)
|
||||
for _, pw := range products {
|
||||
|
||||
if pw.ProjectFlockKandangId != nil && *pw.ProjectFlockKandangId == projectFlockKandang.Id {
|
||||
if pw.ProjectFlockKandangId == nil || *pw.ProjectFlockKandangId == projectFlockKandang.Id {
|
||||
availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw)
|
||||
if err != nil {
|
||||
s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err)
|
||||
@@ -552,6 +593,7 @@ func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validati
|
||||
}
|
||||
|
||||
func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) {
|
||||
|
||||
availableQty := productWarehouse.Quantity
|
||||
|
||||
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
|
||||
@@ -564,7 +606,17 @@ func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehous
|
||||
}
|
||||
}
|
||||
|
||||
availableQty = productWarehouse.Quantity - totalPendingQty
|
||||
totalPopulationQty := 0.0
|
||||
if s.PopulationRepo != nil {
|
||||
popQty, err := s.PopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), productWarehouse.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get population qty for PW %d: %+v", productWarehouse.Id, err)
|
||||
} else {
|
||||
totalPopulationQty = popQty
|
||||
}
|
||||
}
|
||||
|
||||
availableQty = productWarehouse.Quantity - totalPendingQty - totalPopulationQty
|
||||
if availableQty < 0 {
|
||||
availableQty = 0
|
||||
}
|
||||
@@ -578,7 +630,17 @@ func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehous
|
||||
}
|
||||
}
|
||||
|
||||
availableQty = productWarehouse.Quantity - totalPendingQty
|
||||
totalPopulationQty := 0.0
|
||||
if s.PopulationRepo != nil {
|
||||
popQty, err := s.PopulationRepo.GetTotalQtyByProductWarehouseID(c.Context(), productWarehouse.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get population qty for PW %d: %+v", productWarehouse.Id, err)
|
||||
} else {
|
||||
totalPopulationQty = popQty
|
||||
}
|
||||
}
|
||||
|
||||
availableQty = productWarehouse.Quantity - totalPendingQty - totalPopulationQty
|
||||
if availableQty < 0 {
|
||||
availableQty = 0
|
||||
}
|
||||
|
||||
+14
-3
@@ -9,19 +9,17 @@ import (
|
||||
)
|
||||
|
||||
type ProjectFlockPopulationRepository interface {
|
||||
// domain-specific
|
||||
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error)
|
||||
ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error)
|
||||
GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
|
||||
GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
|
||||
GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||
GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
|
||||
GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||
|
||||
// subset of base repository methods used by services
|
||||
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
|
||||
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
|
||||
|
||||
// transaction helpers
|
||||
WithTx(tx *gorm.DB) ProjectFlockPopulationRepository
|
||||
DB() *gorm.DB
|
||||
}
|
||||
@@ -108,6 +106,19 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangI
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error) {
|
||||
var total float64
|
||||
err := r.DB().WithContext(ctx).
|
||||
Model(&entity.ProjectFlockPopulation{}).
|
||||
Where("product_warehouse_id = ?", productWarehouseID).
|
||||
Select("COALESCE(SUM(total_qty), 0)").
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
|
||||
var total float64
|
||||
err := r.DB().WithContext(ctx).
|
||||
|
||||
+14
@@ -28,6 +28,7 @@ type ProjectFlockKandangRepository interface {
|
||||
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
|
||||
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
|
||||
ListIDsByProjectAndKandang(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
|
||||
GetTotalPendingChickinQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
|
||||
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
||||
DB() *gorm.DB
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
@@ -206,6 +207,19 @@ func (r *projectFlockKandangRepositoryImpl) IdExists(ctx context.Context, id uin
|
||||
return repository.Exists[entity.ProjectFlockKandang](ctx, r.db, id)
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) GetTotalPendingChickinQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error) {
|
||||
var total float64
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("project_chickins").
|
||||
Select("COALESCE(SUM(pending_usage_qty), 0)").
|
||||
Where("product_warehouse_id = ?", productWarehouseID).
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) {
|
||||
record := new(entity.ProjectFlockKandang)
|
||||
if err := r.db.WithContext(ctx).
|
||||
|
||||
@@ -2,6 +2,7 @@ package transfer_layings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
||||
sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
|
||||
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
@@ -31,6 +33,44 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
|
||||
productWarehouseRepo := rInventory.NewProductWarehouseRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
|
||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
||||
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
||||
|
||||
|
||||
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
||||
Key: fifo.UsableKeyTransferToLaying,
|
||||
Table: "laying_transfers",
|
||||
Columns: fifo.UsableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
UsageQuantity: "usage_qty",
|
||||
PendingQuantity: "pending_usage_qty",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
}); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
panic(fmt.Sprintf("failed to register transfer to laying usable workflow: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if err := fifoService.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKeyTransferToLaying,
|
||||
Table: "laying_transfers",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "dest_product_warehouse_id",
|
||||
TotalQuantity: "total_qty",
|
||||
TotalUsedQuantity: "total_used",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
OrderBy: []string{"created_at ASC", "id ASC"},
|
||||
}); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
panic(fmt.Sprintf("failed to register transfer to laying stockable workflow: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowTransferToLaying, utils.TransferToLayingApprovalSteps); err != nil {
|
||||
@@ -45,6 +85,7 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val
|
||||
productWarehouseRepo,
|
||||
warehouseRepo,
|
||||
approvalService,
|
||||
fifoService,
|
||||
validate,
|
||||
)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -45,6 +46,7 @@ type transferLayingService struct {
|
||||
ProductWarehouseRepo rInventory.ProductWarehouseRepository
|
||||
WarehouseRepo rWarehouse.WarehouseRepository
|
||||
ApprovalService commonSvc.ApprovalService
|
||||
FifoSvc commonSvc.FifoService
|
||||
}
|
||||
|
||||
func NewTransferLayingService(
|
||||
@@ -55,6 +57,7 @@ func NewTransferLayingService(
|
||||
productWarehouseRepo rInventory.ProductWarehouseRepository,
|
||||
warehouseRepo rWarehouse.WarehouseRepository,
|
||||
approvalService commonSvc.ApprovalService,
|
||||
fifoSvc commonSvc.FifoService,
|
||||
validate *validator.Validate,
|
||||
) TransferLayingService {
|
||||
return &transferLayingService{
|
||||
@@ -67,6 +70,7 @@ func NewTransferLayingService(
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ApprovalService: approvalService,
|
||||
FifoSvc: fifoSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,15 +272,20 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
if len(sourceWarehouseMap) > 0 {
|
||||
for _, pwID := range sourceWarehouseMap {
|
||||
createBody.ProductWarehouseId = &pwID
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Repository.WithTx(dbTransaction).CreateOne(c.Context(), createBody, nil); err != nil {
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
repoTx := s.Repository.WithTx(dbTransaction)
|
||||
|
||||
if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying record")
|
||||
}
|
||||
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
||||
|
||||
for _, sourceDetail := range req.SourceKandangs {
|
||||
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]
|
||||
|
||||
@@ -290,16 +299,11 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source")
|
||||
}
|
||||
|
||||
if err := s.reduceProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, sourceDetail.ProjectFlockKandangId, sourceDetail.Quantity); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reduce project flock population")
|
||||
}
|
||||
|
||||
if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"quantity": gorm.Expr("quantity - ?", sourceDetail.Quantity)}, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update source warehouse quantity")
|
||||
}
|
||||
}
|
||||
|
||||
for _, targetDetail := range req.TargetKandangs {
|
||||
var firstTargetProductWarehouseID *uint
|
||||
|
||||
for i, targetDetail := range req.TargetKandangs {
|
||||
|
||||
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
|
||||
if err != nil {
|
||||
@@ -314,15 +318,41 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
|
||||
}
|
||||
|
||||
var targetPW entity.ProductWarehouse
|
||||
err = dbTransaction.Where("warehouse_id = ? AND project_flock_kandang_id = ?", targetWarehouse.Id, targetDetail.ProjectFlockKandangId).
|
||||
First(&targetPW).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No product warehouse found for target kandang %d in warehouse %d", targetDetail.ProjectFlockKandangId, targetWarehouse.Id))
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err))
|
||||
}
|
||||
|
||||
target := entity.LayingTransferTarget{
|
||||
LayingTransferId: createBody.Id,
|
||||
TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
|
||||
Qty: targetDetail.Quantity,
|
||||
ProductWarehouseId: &targetWarehouse.Id,
|
||||
ProductWarehouseId: &targetPW.Id,
|
||||
}
|
||||
if err := dbTransaction.Create(&target).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target")
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
firstTargetProductWarehouseID = &targetPW.Id
|
||||
}
|
||||
}
|
||||
|
||||
// Set DestProductWarehouseID untuk STOCKABLE role (ambil dari target pertama)
|
||||
if firstTargetProductWarehouseID != nil {
|
||||
createBody.DestProductWarehouseID = firstTargetProductWarehouseID
|
||||
|
||||
// Update DestProductWarehouseID ke database
|
||||
if err := dbTransaction.Model(&entity.LayingTransfer{}).
|
||||
Where("id = ?", createBody.Id).
|
||||
Update("dest_product_warehouse_id", *firstTargetProductWarehouseID).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update DestProductWarehouseID")
|
||||
}
|
||||
}
|
||||
|
||||
if err := createApprovalTransferLaying(c.Context(), dbTransaction, createBody.Id, createBody.CreatedBy); err != nil {
|
||||
@@ -339,7 +369,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
return s.GetOne(c, createBody.Id)
|
||||
}
|
||||
|
||||
func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) {
|
||||
func (s *transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -381,6 +411,7 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
||||
}
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
repoTx := s.Repository.WithTx(dbTransaction)
|
||||
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||
|
||||
@@ -388,7 +419,7 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
||||
if oldSource.ProductWarehouseId != nil && oldSource.Qty > 0 {
|
||||
|
||||
if err := productWarehouseRepoTx.PatchOne(c.Context(), *oldSource.ProductWarehouseId, map[string]any{
|
||||
"quantity": gorm.Expr("quantity + ?", oldSource.Qty),
|
||||
"qty": gorm.Expr("qty + ?", oldSource.Qty),
|
||||
}, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore warehouse quantity")
|
||||
}
|
||||
@@ -416,7 +447,7 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
||||
totalSourceQty += source.Quantity
|
||||
}
|
||||
|
||||
if err := s.Repository.WithTx(dbTransaction).PatchOne(c.Context(), id, map[string]any{
|
||||
if err := repoTx.PatchOne(c.Context(), id, map[string]any{
|
||||
"transfer_date": transferDate,
|
||||
"notes": req.Reason,
|
||||
"pending_usage_qty": &totalSourceQty,
|
||||
@@ -466,7 +497,7 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
||||
return err
|
||||
}
|
||||
|
||||
if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"quantity": gorm.Expr("quantity - ?", sourceDetail.Quantity)}, nil); err != nil {
|
||||
if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"qty": gorm.Expr("qty - ?", sourceDetail.Quantity)}, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update source warehouse quantity")
|
||||
}
|
||||
}
|
||||
@@ -485,11 +516,21 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
|
||||
}
|
||||
|
||||
var targetPW entity.ProductWarehouse
|
||||
err = dbTransaction.Where("warehouse_id = ? AND project_flock_kandang_id = ?", targetWarehouse.Id, targetDetail.ProjectFlockKandangId).
|
||||
First(&targetPW).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No product warehouse found for target kandang %d in warehouse %d", targetDetail.ProjectFlockKandangId, targetWarehouse.Id))
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for target kandang %d: %v", targetDetail.ProjectFlockKandangId, err))
|
||||
}
|
||||
|
||||
target := entity.LayingTransferTarget{
|
||||
LayingTransferId: id,
|
||||
TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
|
||||
Qty: targetDetail.Quantity,
|
||||
ProductWarehouseId: &targetWarehouse.Id,
|
||||
ProductWarehouseId: &targetPW.Id,
|
||||
}
|
||||
if err := dbTransaction.Create(&target).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target")
|
||||
@@ -531,8 +572,9 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
}
|
||||
}
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
|
||||
repoTx := s.Repository.WithTx(dbTransaction)
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
||||
|
||||
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
||||
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), id)
|
||||
@@ -544,14 +586,13 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
if source.ProductWarehouseId != nil && source.Qty > 0 {
|
||||
|
||||
if err := productWarehouseRepoTx.PatchOne(c.Context(), *source.ProductWarehouseId, map[string]any{
|
||||
"quantity": gorm.Expr("quantity + ?", source.Qty),
|
||||
"qty": gorm.Expr("qty + ?", source.Qty),
|
||||
}, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore source warehouse quantity")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
||||
for _, source := range sources {
|
||||
populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -575,7 +616,7 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Repository.WithTx(dbTransaction).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")
|
||||
}
|
||||
|
||||
@@ -624,14 +665,13 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
||||
}
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
|
||||
repoTx := s.Repository.WithTx(dbTransaction)
|
||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
||||
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||
|
||||
for _, approvableID := range approvableIDs {
|
||||
transfer, err := s.Repository.GetByID(c.Context(), approvableID, nil)
|
||||
transfer, err := repoTx.GetByID(c.Context(), approvableID, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("TransferLaying %d not found", approvableID))
|
||||
@@ -664,43 +704,45 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
||||
}
|
||||
|
||||
if len(sources) > 0 && len(targets) > 0 {
|
||||
firstSource := sources[0]
|
||||
if firstSource.ProductWarehouseId == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID))
|
||||
}
|
||||
|
||||
sourceWarehouse, err := productWarehouseRepoTx.GetByID(c.Context(), *firstSource.ProductWarehouseId, nil)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source warehouse")
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
|
||||
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), target.TargetProjectFlockKandangId)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
continue
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
|
||||
for _, source := range sources {
|
||||
if source.ProductWarehouseId == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID))
|
||||
}
|
||||
|
||||
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId)
|
||||
_, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
|
||||
UsableKey: fifo.UsableKeyTransferToLaying,
|
||||
UsableID: approvableID,
|
||||
ProductWarehouseID: *source.ProductWarehouseId,
|
||||
Quantity: source.Qty,
|
||||
AllowPending: false,
|
||||
Tx: dbTransaction,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
continue
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to consume FIFO stock for source %d: %v", source.ProductWarehouseId, err))
|
||||
}
|
||||
}
|
||||
|
||||
if transfer.DestProductWarehouseID != nil {
|
||||
note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber)
|
||||
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||
StockableKey: fifo.StockableKeyTransferToLaying,
|
||||
StockableID: approvableID,
|
||||
ProductWarehouseID: *transfer.DestProductWarehouseID,
|
||||
Quantity: *transfer.PendingUsageQty,
|
||||
Note: ¬e,
|
||||
Tx: dbTransaction,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock to destination warehouse: %v", err))
|
||||
}
|
||||
|
||||
if _, err := s.getOrCreateProductWarehouse(
|
||||
c.Context(),
|
||||
dbTransaction,
|
||||
sourceWarehouse.ProductId,
|
||||
targetWarehouse.Id,
|
||||
target.Qty,
|
||||
actorID,
|
||||
); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create or update product warehouse")
|
||||
if err := dbTransaction.Model(&entity.LayingTransfer{}).
|
||||
Where("id = ?", approvableID).
|
||||
Updates(map[string]interface{}{
|
||||
"total_qty": replenishResult.AddedQuantity,
|
||||
}).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update total quantity for transfer")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -708,9 +750,10 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
||||
usageQty := *transfer.PendingUsageQty
|
||||
updateData := map[string]any{
|
||||
"usage_qty": usageQty,
|
||||
"total_qty": usageQty, // Same as usage_qty for initial transfer
|
||||
"pending_usage_qty": nil,
|
||||
}
|
||||
if err := s.Repository.WithTx(dbTransaction).PatchOne(c.Context(), approvableID, updateData, nil); err != nil {
|
||||
if err := repoTx.PatchOne(c.Context(), approvableID, updateData, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying status")
|
||||
}
|
||||
}
|
||||
@@ -758,14 +801,14 @@ func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayi
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, tx *gorm.DB, productID uint, warehouseID uint, quantity float64, actorID uint) (*entity.ProductWarehouse, error) {
|
||||
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{"quantity": gorm.Expr("quantity + ?", quantity)}, nil); err != 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
|
||||
@@ -775,9 +818,10 @@ func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context,
|
||||
}
|
||||
|
||||
newWarehouse := &entity.ProductWarehouse{
|
||||
ProductId: productID,
|
||||
WarehouseId: warehouseID,
|
||||
Quantity: quantity,
|
||||
ProductId: productID,
|
||||
WarehouseId: warehouseID,
|
||||
ProjectFlockKandangId: projectFlockKandangId, // Set flock ID agar bisa di-chickin di target flock
|
||||
Quantity: quantity,
|
||||
// CreatedBy: actorID,
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
|
||||
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||
_ = fifoService.RegisterStockable(fifo.StockableConfig{
|
||||
Key: fifo.StockableKey("PURCHASE_ITEMS"),
|
||||
Key: fifo.StockableKeyPurchaseItems,
|
||||
Table: "purchase_items",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
|
||||
@@ -42,8 +42,7 @@ type PurchaseService interface {
|
||||
}
|
||||
|
||||
const (
|
||||
priceTolerance = 0.0001
|
||||
purchaseStockableKey = fifo.StockableKey("PURCHASE_ITEMS")
|
||||
priceTolerance = 0.0001
|
||||
)
|
||||
|
||||
type purchaseService struct {
|
||||
@@ -924,7 +923,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
continue
|
||||
}
|
||||
if _, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||
StockableKey: purchaseStockableKey,
|
||||
StockableKey: fifo.StockableKeyPurchaseItems,
|
||||
StockableID: adj.itemID,
|
||||
ProductWarehouseID: adj.pwID,
|
||||
Quantity: adj.qty,
|
||||
|
||||
@@ -106,7 +106,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
||||
Where("r.deleted_at IS NULL")
|
||||
recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs)
|
||||
|
||||
purchaseStockableKey := fifo.StockableKey("PURCHASE_ITEMS").String()
|
||||
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
|
||||
transferStockableKey := fifo.StockableKey("STOCK_TRANSFER_DETAILS").String()
|
||||
|
||||
query := r.db.WithContext(ctx).
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
package fifo
|
||||
|
||||
const (
|
||||
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
||||
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
|
||||
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
|
||||
// Usable Keys
|
||||
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
||||
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
|
||||
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
|
||||
UsableKeyTransferToLaying UsableKey = "TRANSFER_TO_LAYING"
|
||||
UsableKeyStockTransferOut UsableKey = "STOCK_TRANSFER_OUT"
|
||||
UsableKeyAdjustmentOut UsableKey = "ADJUSTMENT_OUT"
|
||||
|
||||
// Stockable Keys
|
||||
StockableKeyTransferToLaying StockableKey = "TRANSFER_TO_LAYING"
|
||||
StockableKeyStockTransferIn StockableKey = "STOCK_TRANSFER_IN"
|
||||
StockableKeyAdjustmentIn StockableKey = "ADJUSTMENT_IN"
|
||||
StockableKeyPurchaseItems StockableKey = "PURCHASE_ITEMS"
|
||||
StockableKeyProjectFlockPopulation StockableKey = "PROJECT_FLOCK_POPULATION"
|
||||
)
|
||||
|
||||
@@ -54,11 +54,12 @@ type StockableConfig struct {
|
||||
|
||||
// UsableConfig registers a table that consumes stock (recordings, adjustments, sales, etc).
|
||||
type UsableConfig struct {
|
||||
Key UsableKey
|
||||
Table string
|
||||
Columns UsableColumns
|
||||
OrderBy []string
|
||||
Scope QueryScope
|
||||
Key UsableKey
|
||||
Table string
|
||||
Columns UsableColumns
|
||||
OrderBy []string
|
||||
Scope QueryScope
|
||||
ExcludedStockables []StockableKey // Stockables to exclude when consuming stock
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
Reference in New Issue
Block a user