mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +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 {
|
switch {
|
||||||
case delta > 0:
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -410,8 +416,9 @@ func (s *fifoService) allocateFromStock(
|
|||||||
usableKey fifo.UsableKey,
|
usableKey fifo.UsableKey,
|
||||||
usableID uint,
|
usableID uint,
|
||||||
requestQty float64,
|
requestQty float64,
|
||||||
|
excludedStockables []fifo.StockableKey,
|
||||||
) (*allocationOutcome, error) {
|
) (*allocationOutcome, error) {
|
||||||
lots, err := s.fetchStockLots(ctx, tx, productWarehouseID)
|
lots, err := s.fetchStockLots(ctx, tx, productWarehouseID, excludedStockables)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -492,14 +499,24 @@ func (s *fifoService) allocateFromStock(
|
|||||||
}, nil
|
}, 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()
|
configs := fifo.Stockables()
|
||||||
if len(configs) == 0 {
|
if len(configs) == 0 {
|
||||||
return nil, nil
|
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
|
var lots []stockLot
|
||||||
for key, cfg := range configs {
|
for key, cfg := range configs {
|
||||||
|
// Skip excluded stockables
|
||||||
|
if excludedSet[key] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
|
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
|
||||||
|
|
||||||
@@ -616,7 +633,13 @@ func (s *fifoService) resolvePendingForWarehouse(ctx context.Context, tx *gorm.D
|
|||||||
continue
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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"`
|
FromProjectFlockId uint `gorm:"not null"`
|
||||||
ToProjectFlockId uint `gorm:"not null"`
|
ToProjectFlockId uint `gorm:"not null"`
|
||||||
TransferDate time.Time `gorm:"type:date;not null"`
|
TransferDate time.Time `gorm:"type:date;not null"`
|
||||||
|
|
||||||
|
|
||||||
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
|
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
|
||||||
UsageQty *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"`
|
Notes string `gorm:"type:text"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
|
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
|
||||||
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
|
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` // Source PW
|
||||||
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID;references:Id"` // Destination PW
|
||||||
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
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"`
|
ClosedAt *time.Time `gorm:"index"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
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"
|
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
|
||||||
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
@@ -32,6 +33,7 @@ type ExpenseBaseDTO struct {
|
|||||||
|
|
||||||
type ExpenseListDTO struct {
|
type ExpenseListDTO struct {
|
||||||
ExpenseBaseDTO
|
ExpenseBaseDTO
|
||||||
|
GrandTotal float64 `json:"grand_total"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -140,8 +142,11 @@ func ToExpenseListDTO(e entity.Expense) ExpenseListDTO {
|
|||||||
latestApproval = &mapped
|
latestApproval = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
|
grandTotal := calculateGrandTotal(&e)
|
||||||
|
|
||||||
return ExpenseListDTO{
|
return ExpenseListDTO{
|
||||||
ExpenseBaseDTO: ToExpenseBaseDTO(&e),
|
ExpenseBaseDTO: ToExpenseBaseDTO(&e),
|
||||||
|
GrandTotal: grandTotal,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
@@ -344,3 +349,25 @@ func ToKandangGroupDTO(pengajuans []ExpenseNonstockDTO, realisasi []ExpenseReali
|
|||||||
|
|
||||||
return kandangs
|
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) {
|
if !hasKandang && req.Category == string(utils.ExpenseCategoryBOP) {
|
||||||
projectFlockRepoTx := projectFlockKandangRepo.NewProjectflockRepository(dbTransaction)
|
projectFlockRepoTx := projectFlockKandangRepo.NewProjectflockRepository(dbTransaction)
|
||||||
activeProjectFlocks, err := projectFlockRepoTx.GetActiveByLocationID(c.Context(), req.LocationID)
|
activeProjectFlocks, err := projectFlockRepoTx.GetActiveByLocationID(c.Context(), req.LocationID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get active project flocks for location")
|
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)
|
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||||
|
|
||||||
err := fifoService.RegisterStockable(fifo.StockableConfig{
|
err := fifoService.RegisterStockable(fifo.StockableConfig{
|
||||||
Key: fifo.StockableKey("ADJUSTMENT_IN"),
|
Key: fifo.StockableKeyAdjustmentIn,
|
||||||
Table: "adjustment_stocks",
|
Table: "adjustment_stocks",
|
||||||
Columns: fifo.StockableColumns{
|
Columns: fifo.StockableColumns{
|
||||||
ID: "id",
|
ID: "id",
|
||||||
@@ -52,7 +52,7 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = fifoService.RegisterUsable(fifo.UsableConfig{
|
err = fifoService.RegisterUsable(fifo.UsableConfig{
|
||||||
Key: fifo.UsableKey("ADJUSTMENT_OUT"),
|
Key: fifo.UsableKeyAdjustmentOut,
|
||||||
Table: "adjustment_stocks",
|
Table: "adjustment_stocks",
|
||||||
Columns: fifo.UsableColumns{
|
Columns: fifo.UsableColumns{
|
||||||
ID: "id",
|
ID: "id",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ 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"
|
||||||
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/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"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -123,15 +124,9 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
projectFlockKandangID = &pfkID
|
projectFlockKandangID = &pfkID
|
||||||
}
|
}
|
||||||
|
|
||||||
pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(
|
pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(ctx, uint(req.ProductID), uint(req.WarehouseID), projectFlockKandangID)
|
||||||
ctx,
|
|
||||||
uint(req.ProductID),
|
|
||||||
uint(req.WarehouseID),
|
|
||||||
projectFlockKandangID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
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")
|
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 {
|
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")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse")
|
||||||
}
|
}
|
||||||
pw = newPW
|
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")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create StockLog for history tracking
|
|
||||||
afterQuantity := productWarehouse.Quantity
|
afterQuantity := productWarehouse.Quantity
|
||||||
newLog := &entity.StockLog{
|
newLog := &entity.StockLog{
|
||||||
LoggableType: string(utils.StockLogTypeAdjustment),
|
LoggableType: string(utils.StockLogTypeAdjustment),
|
||||||
@@ -189,7 +182,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create AdjustmentStock record for FIFO tracking
|
|
||||||
adjustmentStock := &entity.AdjustmentStock{
|
adjustmentStock := &entity.AdjustmentStock{
|
||||||
StockLogId: newLog.Id,
|
StockLogId: newLog.Id,
|
||||||
ProductWarehouseId: productWarehouse.Id,
|
ProductWarehouseId: productWarehouse.Id,
|
||||||
@@ -200,10 +192,10 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
||||||
// Adjustment INCREASE → Replenish stock (Stockable)
|
|
||||||
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
|
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
|
||||||
_, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
|
_, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
|
||||||
StockableKey: "ADJUSTMENT_IN",
|
StockableKey: fifo.StockableKeyAdjustmentIn,
|
||||||
StockableID: adjustmentStock.Id,
|
StockableID: adjustmentStock.Id,
|
||||||
ProductWarehouseID: uint(productWarehouse.Id),
|
ProductWarehouseID: uint(productWarehouse.Id),
|
||||||
Quantity: req.Quantity,
|
Quantity: req.Quantity,
|
||||||
@@ -215,9 +207,8 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Adjustment DECREASE → Consume stock (Usable)
|
|
||||||
_, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
|
_, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
|
||||||
UsableKey: "ADJUSTMENT_OUT",
|
UsableKey: fifo.UsableKeyAdjustmentOut,
|
||||||
UsableID: adjustmentStock.Id,
|
UsableID: adjustmentStock.Id,
|
||||||
ProductWarehouseID: uint(productWarehouse.Id),
|
ProductWarehouseID: uint(productWarehouse.Id),
|
||||||
Quantity: req.Quantity,
|
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)
|
// Update ProductWarehouse quantity (for backward compatibility/reporting)
|
||||||
|
|
||||||
productWarehouse.Quantity = afterQuantity
|
productWarehouse.Quantity = afterQuantity
|
||||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil {
|
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)
|
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
|
||||||
|
|||||||
@@ -235,6 +235,15 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
|||||||
|
|
||||||
var deliveries []TransferDeliveryDTO
|
var deliveries []TransferDeliveryDTO
|
||||||
for _, del := range e.Deliveries {
|
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
|
var document *DocumentDTO
|
||||||
if len(del.Documents) > 0 {
|
if len(del.Documents) > 0 {
|
||||||
doc := del.Documents[0] // Take first document
|
doc := del.Documents[0] // Take first document
|
||||||
@@ -258,6 +267,7 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
|||||||
DocumentNumber: del.DocumentNumber,
|
DocumentNumber: del.DocumentNumber,
|
||||||
ShippingCostItem: del.ShippingCostItem,
|
ShippingCostItem: del.ShippingCostItem,
|
||||||
ShippingCostTotal: del.ShippingCostTotal,
|
ShippingCostTotal: del.ShippingCostTotal,
|
||||||
|
Items: items,
|
||||||
Document: document,
|
Document: document,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,12 +43,10 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize FIFO Service
|
|
||||||
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||||
|
|
||||||
// Register Transfer as Stockable (adds stock to destination warehouse)
|
|
||||||
err = fifoService.RegisterStockable(fifo.StockableConfig{
|
err = fifoService.RegisterStockable(fifo.StockableConfig{
|
||||||
Key: fifo.StockableKey("STOCK_TRANSFER_IN"),
|
Key: fifo.StockableKeyStockTransferIn,
|
||||||
Table: "stock_transfer_details",
|
Table: "stock_transfer_details",
|
||||||
Columns: fifo.StockableColumns{
|
Columns: fifo.StockableColumns{
|
||||||
ID: "id",
|
ID: "id",
|
||||||
@@ -63,9 +61,8 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register Transfer as Usable (consumes stock from source warehouse)
|
|
||||||
err = fifoService.RegisterUsable(fifo.UsableConfig{
|
err = fifoService.RegisterUsable(fifo.UsableConfig{
|
||||||
Key: fifo.UsableKey("STOCK_TRANSFER_OUT"),
|
Key: fifo.UsableKeyStockTransferOut,
|
||||||
Table: "stock_transfer_details",
|
Table: "stock_transfer_details",
|
||||||
Columns: fifo.UsableColumns{
|
Columns: fifo.UsableColumns{
|
||||||
ID: "id",
|
ID: "id",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ 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"
|
||||||
)
|
)
|
||||||
@@ -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 {
|
for _, product := range req.Products {
|
||||||
detail := detailMap[uint64(product.ProductID)]
|
detail := detailMap[uint64(product.ProductID)]
|
||||||
|
|
||||||
// Step 1: Consume stock from source warehouse (STOCK_TRANSFER_OUT)
|
|
||||||
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
|
consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{
|
||||||
UsableKey: "STOCK_TRANSFER_OUT",
|
UsableKey: fifo.UsableKeyStockTransferOut,
|
||||||
UsableID: uint(detail.Id),
|
UsableID: uint(detail.Id),
|
||||||
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
|
ProductWarehouseID: uint(*detail.SourceProductWarehouseID),
|
||||||
Quantity: product.ProductQty,
|
Quantity: product.ProductQty,
|
||||||
AllowPending: false, // Don't allow pending, must have actual stock
|
AllowPending: false,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok tidak cukup di gudang asal untuk produk %d: %v", product.ProductID, err))
|
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{}).
|
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||||
Where("id = ?", detail.Id).
|
Where("id = ?", detail.Id).
|
||||||
Updates(map[string]interface{}{
|
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)
|
// Step 2: Replenish stock to destination warehouse (STOCK_TRANSFER_IN)
|
||||||
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
note := fmt.Sprintf("Transfer #%s", entityTransfer.MovementNumber)
|
||||||
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
replenishResult, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||||
StockableKey: "STOCK_TRANSFER_IN",
|
StockableKey: fifo.StockableKeyStockTransferIn,
|
||||||
StockableID: uint(detail.Id),
|
StockableID: uint(detail.Id),
|
||||||
ProductWarehouseID: uint(*detail.DestProductWarehouseID),
|
ProductWarehouseID: uint(*detail.DestProductWarehouseID),
|
||||||
Quantity: product.ProductQty,
|
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))
|
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{}).
|
if err := tx.Model(&entity.StockTransferDetail{}).
|
||||||
Where("id = ?", detail.Id).
|
Where("id = ?", detail.Id).
|
||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
|
|||||||
@@ -52,12 +52,31 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
PendingQuantity: "pending_usage_qty",
|
PendingQuantity: "pending_usage_qty",
|
||||||
CreatedAt: "created_at",
|
CreatedAt: "created_at",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ExcludedStockables: []fifo.StockableKey{fifo.StockableKeyProjectFlockPopulation},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||||
panic(fmt.Sprintf("failed to register chickin usable workflow: %v", err))
|
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)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil {
|
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)
|
GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
||||||
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||||
GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID 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 {
|
type ChickinRepositoryImpl struct {
|
||||||
@@ -64,6 +66,26 @@ func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context,
|
|||||||
return chickins, nil
|
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) {
|
func (r *ChickinRepositoryImpl) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
|
||||||
var chickins []entity.ProjectChickin
|
var chickins []entity.ProjectChickin
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
@@ -102,3 +124,13 @@ func (r *ChickinRepositoryImpl) GetTotalChickinQtyByProjectFlockID(ctx context.C
|
|||||||
Scan(&result).Error
|
Scan(&result).Error
|
||||||
return result, err
|
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")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed get chickin by id: %+v", err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return chickin, nil
|
return chickin, nil
|
||||||
@@ -137,12 +136,15 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
newChikins := make([]*entity.ProjectChickin, 0)
|
newChikins := make([]*entity.ProjectChickin, 0)
|
||||||
chickinQtyMap := make(map[uint]float64)
|
chickinQtyMap := make(map[uint]float64)
|
||||||
|
|
||||||
for idx, chickinReq := range req.ChickinRequests {
|
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 {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickinReq.ProductWarehouseId))
|
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))
|
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 {
|
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))
|
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)
|
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))
|
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{
|
newChickin := &entity.ProjectChickin{
|
||||||
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
||||||
ChickInDate: chickinDate,
|
ChickInDate: chickinDate,
|
||||||
@@ -176,6 +197,17 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
}
|
}
|
||||||
|
|
||||||
newChikins = append(newChikins, newChickin)
|
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
|
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")
|
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 {
|
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))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
|
||||||
if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil {
|
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 {
|
if chickin.UsageQty > 0 {
|
||||||
|
|
||||||
|
currentUsageQty := chickin.UsageQty
|
||||||
|
|
||||||
if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil {
|
if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
warehouseDeltas := make(map[uint]float64)
|
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 {
|
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)
|
s.Log.Errorf("Failed to adjust product warehouses for deleted chickin %d: %+v", chickin.Id, err)
|
||||||
return err
|
return err
|
||||||
@@ -552,6 +612,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
|||||||
}
|
}
|
||||||
|
|
||||||
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
|
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
|
||||||
|
chickinRepoTx := s.Repository.WithTx(dbTransaction)
|
||||||
|
|
||||||
var totalQuantityAdded float64
|
var totalQuantityAdded float64
|
||||||
|
|
||||||
@@ -572,7 +633,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
|||||||
population := &entity.ProjectFlockPopulation{
|
population := &entity.ProjectFlockPopulation{
|
||||||
ProjectChickinId: chickin.Id,
|
ProjectChickinId: chickin.Id,
|
||||||
ProductWarehouseId: targetPW.Id,
|
ProductWarehouseId: targetPW.Id,
|
||||||
TotalQty: quantityToConvert,
|
TotalQty: 0, // Will be set by FIFO Replenish
|
||||||
TotalUsedQty: 0,
|
TotalUsedQty: 0,
|
||||||
Notes: chickin.Notes,
|
Notes: chickin.Notes,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
@@ -581,18 +642,29 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
|||||||
return err
|
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
|
totalQuantityAdded += quantityToConvert
|
||||||
}
|
}
|
||||||
|
|
||||||
if totalQuantityAdded > 0 {
|
// NOTE: ProductWarehouse target sudah ditambah melalui ReplenishChickinStocks
|
||||||
if err := s.ProductWarehouseRepo.AdjustQuantities(ctx.Context(), map[uint]float64{
|
// yang dipanggil di atas untuk setiap chickin berdasarkan flag source:
|
||||||
targetPW.Id: totalQuantityAdded,
|
// - DOC → replenish ke PULLET
|
||||||
}, func(db *gorm.DB) *gorm.DB {
|
// - PULLET → replenish ke LAYER
|
||||||
return dbTransaction
|
// - LAYER → tidak perlu replenish (sudah final)
|
||||||
}); err != nil {
|
// - DOC+PULLET+LAYER → replenish ke dirinya sendiri
|
||||||
return fmt.Errorf("failed to update target product warehouse quantity: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
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",
|
s.Log.Infof("ConsumeChickinStocks result: usage_qty=%.3f, pending_qty=%.3f, allocated_allocations=%d",
|
||||||
result.UsageQuantity, result.PendingQuantity, len(result.AddedAllocations))
|
result.UsageQuantity, result.PendingQuantity, len(result.AddedAllocations))
|
||||||
|
|
||||||
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Updates(map[string]interface{}{
|
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
||||||
"usage_qty": result.UsageQuantity,
|
|
||||||
"pending_usage_qty": result.PendingQuantity,
|
|
||||||
}).Error; err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,6 +715,101 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
return nil
|
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 {
|
func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, actorID uint) error {
|
||||||
if chickin == nil || s.FifoSvc == nil {
|
if chickin == nil || s.FifoSvc == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -653,8 +817,7 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
|
|
||||||
var currentUsage float64
|
var currentUsage float64
|
||||||
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(¤tUsage).Error; err != nil {
|
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{
|
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
|
||||||
@@ -666,14 +829,10 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Updates(map[string]interface{}{
|
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, 0); err != nil {
|
||||||
"usage_qty": 0,
|
|
||||||
"pending_usage_qty": 0,
|
|
||||||
}).Error; err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create stock log for the restoration
|
|
||||||
if currentUsage > 0 {
|
if currentUsage > 0 {
|
||||||
increaseLog := &entity.StockLog{
|
increaseLog := &entity.StockLog{
|
||||||
Increase: currentUsage,
|
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),
|
Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id),
|
||||||
}
|
}
|
||||||
if err := s.StockLogRepo.CreateOne(ctx, increaseLog, nil); err != nil {
|
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)
|
s.Log.Errorf("Failed to create stock log for released chickin %d: %+v", chickin.Id, err)
|
||||||
// Don't return error here, stock already released
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+27
-27
@@ -28,14 +28,14 @@ type ProjectFlockKandangRelationDTO struct {
|
|||||||
|
|
||||||
type ProjectFlockDTO struct {
|
type ProjectFlockDTO struct {
|
||||||
projectFlockDTO.ProjectFlockRelationDTO
|
projectFlockDTO.ProjectFlockRelationDTO
|
||||||
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
|
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"`
|
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"`
|
||||||
ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"`
|
ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"`
|
||||||
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductWarehouseDTO struct {
|
type ProductWarehouseDTO struct {
|
||||||
@@ -51,11 +51,12 @@ type AvailableQtyDTO struct {
|
|||||||
|
|
||||||
type ProjectFlockKandangListDTO struct {
|
type ProjectFlockKandangListDTO struct {
|
||||||
ProjectFlockKandangRelationDTO
|
ProjectFlockKandangRelationDTO
|
||||||
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
|
ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"`
|
||||||
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
|
Approval *approvalDTO.ApprovalRelationDTO `json:"approval,omitempty"`
|
||||||
|
ChickinApproval *approvalDTO.ApprovalRelationDTO `json:"chickin_approval,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectFlockKandangDetailDTO struct {
|
type ProjectFlockKandangDetailDTO struct {
|
||||||
@@ -105,7 +106,8 @@ func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang
|
|||||||
Kandang: toKandangRelation(e.Kandang),
|
Kandang: toKandangRelation(e.Kandang),
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
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{
|
return ProjectFlockKandangDetailDTO{
|
||||||
@@ -124,9 +126,11 @@ func toKandangRelation(kandang entity.Kandang) *kandangDTO.KandangRelationDTO {
|
|||||||
return &mapped
|
return &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalRelationDTO {
|
func toApprovalDTOSelector(
|
||||||
if e.LatestApproval != nil {
|
e entity.ProjectFlockKandang, selector func(entity.ProjectFlockKandang) *entity.Approval) *approvalDTO.ApprovalRelationDTO {
|
||||||
mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
approval := selector(e)
|
||||||
|
if approval != nil {
|
||||||
|
mapped := approvalDTO.ToApprovalDTO(*approval)
|
||||||
return &mapped
|
return &mapped
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -145,18 +149,11 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand
|
|||||||
Kandang: toKandangRelation(e.Kandang),
|
Kandang: toKandangRelation(e.Kandang),
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
CreatedUser: toCreatedUserDTO(e.ProjectFlock),
|
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 {
|
func toCreatedUserDTO(pf entity.ProjectFlock) *userDTO.UserRelationDTO {
|
||||||
if pf.CreatedUser.Id != 0 {
|
if pf.CreatedUser.Id != 0 {
|
||||||
mapped := userDTO.ToUserRelationDTO(pf.CreatedUser)
|
mapped := userDTO.ToUserRelationDTO(pf.CreatedUser)
|
||||||
@@ -187,7 +184,6 @@ func toAvailableQtyDTOsFromMap(chickins []entity.ProjectChickin, availableQtyMap
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, build map from chickins
|
|
||||||
pwMap := make(map[uint]*entity.ProductWarehouse)
|
pwMap := make(map[uint]*entity.ProductWarehouse)
|
||||||
for _, chickin := range chickins {
|
for _, chickin := range chickins {
|
||||||
if chickin.ProductWarehouse != nil && chickin.ProductWarehouse.Id != 0 {
|
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 {
|
for i := range productWarehouses {
|
||||||
if _, exists := pwMap[productWarehouses[i].Id]; !exists {
|
if _, exists := pwMap[productWarehouses[i].Id]; !exists {
|
||||||
pwMap[productWarehouses[i].Id] = &productWarehouses[i]
|
pwMap[productWarehouses[i].Id] = &productWarehouses[i]
|
||||||
@@ -204,6 +199,11 @@ func toAvailableQtyDTOsFromMap(chickins []entity.ProjectChickin, availableQtyMap
|
|||||||
|
|
||||||
result := make([]AvailableQtyDTO, 0, len(availableQtyMap))
|
result := make([]AvailableQtyDTO, 0, len(availableQtyMap))
|
||||||
for pwId, availableQty := range availableQtyMap {
|
for pwId, availableQty := range availableQtyMap {
|
||||||
|
|
||||||
|
if availableQty <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
pw, exists := pwMap[pwId]
|
pw, exists := pwMap[pwId]
|
||||||
if !exists || pw == nil {
|
if !exists || pw == nil {
|
||||||
continue
|
continue
|
||||||
|
|||||||
+90
-28
@@ -98,23 +98,8 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.ApprovalSvc != nil {
|
if s.ApprovalSvc != nil {
|
||||||
projectFlockKandangIDs := make([]uint, len(projectFlockKandangs))
|
s.fetchProjectFlockApprovals(c, projectFlockKandangs)
|
||||||
for i, pfk := range projectFlockKandangs {
|
s.fetchChickinApprovals(c, 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return projectFlockKandangs, total, nil
|
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 {
|
if len(projectFlockKandang.Chickins) > 0 && s.ApprovalSvc != nil {
|
||||||
latest, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
|
s.fetchProjectFlockApproval(c, projectFlockKandang)
|
||||||
if err != nil {
|
s.fetchChickinApproval(c, projectFlockKandang)
|
||||||
s.Log.Errorf("Failed to fetch latest kandang approval for projectFlockKandang %d: %+v", projectFlockKandang.Id, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if latest != nil {
|
|
||||||
projectFlockKandang.LatestApproval = latest
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
availableQtyMap, err := s.getAvailableQuantities(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
|
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) {
|
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 {
|
if projectFlockKandang.Kandang.Id == 0 || s.WarehouseRepo == nil || s.ProductWarehouseRepo == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -191,7 +232,7 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project
|
|||||||
result := make(map[uint]float64)
|
result := make(map[uint]float64)
|
||||||
for _, pw := range products {
|
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)
|
availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err)
|
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) {
|
func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) {
|
||||||
|
|
||||||
availableQty := productWarehouse.Quantity
|
availableQty := productWarehouse.Quantity
|
||||||
|
|
||||||
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
|
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 {
|
if availableQty < 0 {
|
||||||
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 {
|
if availableQty < 0 {
|
||||||
availableQty = 0
|
availableQty = 0
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-3
@@ -9,19 +9,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProjectFlockPopulationRepository interface {
|
type ProjectFlockPopulationRepository interface {
|
||||||
// domain-specific
|
|
||||||
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error)
|
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectFlockPopulation, error)
|
||||||
ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error)
|
ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error)
|
||||||
GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
|
GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error)
|
||||||
GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID 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)
|
GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||||
|
GetTotalQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
|
||||||
GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID 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
|
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
|
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
|
||||||
|
|
||||||
// transaction helpers
|
|
||||||
WithTx(tx *gorm.DB) ProjectFlockPopulationRepository
|
WithTx(tx *gorm.DB) ProjectFlockPopulationRepository
|
||||||
DB() *gorm.DB
|
DB() *gorm.DB
|
||||||
}
|
}
|
||||||
@@ -108,6 +106,19 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangI
|
|||||||
return total, nil
|
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) {
|
func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) {
|
||||||
var total float64
|
var total float64
|
||||||
err := r.DB().WithContext(ctx).
|
err := r.DB().WithContext(ctx).
|
||||||
|
|||||||
+14
@@ -28,6 +28,7 @@ type ProjectFlockKandangRepository interface {
|
|||||||
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
|
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
|
||||||
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
|
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
|
||||||
ListIDsByProjectAndKandang(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
|
ListIDsByProjectAndKandang(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
|
||||||
|
GetTotalPendingChickinQtyByProductWarehouseID(ctx context.Context, productWarehouseID uint) (float64, error)
|
||||||
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
||||||
DB() *gorm.DB
|
DB() *gorm.DB
|
||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
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)
|
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) {
|
func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) {
|
||||||
record := new(entity.ProjectFlockKandang)
|
record := new(entity.ProjectFlockKandang)
|
||||||
if err := r.db.WithContext(ctx).
|
if err := r.db.WithContext(ctx).
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package transfer_layings
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
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"
|
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"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
|
|
||||||
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
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"
|
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)
|
productWarehouseRepo := rInventory.NewProductWarehouseRepository(db)
|
||||||
warehouseRepo := rWarehouse.NewWarehouseRepository(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)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowTransferToLaying, utils.TransferToLayingApprovalSteps); err != nil {
|
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,
|
productWarehouseRepo,
|
||||||
warehouseRepo,
|
warehouseRepo,
|
||||||
approvalService,
|
approvalService,
|
||||||
|
fifoService,
|
||||||
validate,
|
validate,
|
||||||
)
|
)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
|
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"
|
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"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -45,6 +46,7 @@ type transferLayingService struct {
|
|||||||
ProductWarehouseRepo rInventory.ProductWarehouseRepository
|
ProductWarehouseRepo rInventory.ProductWarehouseRepository
|
||||||
WarehouseRepo rWarehouse.WarehouseRepository
|
WarehouseRepo rWarehouse.WarehouseRepository
|
||||||
ApprovalService commonSvc.ApprovalService
|
ApprovalService commonSvc.ApprovalService
|
||||||
|
FifoSvc commonSvc.FifoService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransferLayingService(
|
func NewTransferLayingService(
|
||||||
@@ -55,6 +57,7 @@ func NewTransferLayingService(
|
|||||||
productWarehouseRepo rInventory.ProductWarehouseRepository,
|
productWarehouseRepo rInventory.ProductWarehouseRepository,
|
||||||
warehouseRepo rWarehouse.WarehouseRepository,
|
warehouseRepo rWarehouse.WarehouseRepository,
|
||||||
approvalService commonSvc.ApprovalService,
|
approvalService commonSvc.ApprovalService,
|
||||||
|
fifoSvc commonSvc.FifoService,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
) TransferLayingService {
|
) TransferLayingService {
|
||||||
return &transferLayingService{
|
return &transferLayingService{
|
||||||
@@ -67,6 +70,7 @@ func NewTransferLayingService(
|
|||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ApprovalService: approvalService,
|
ApprovalService: approvalService,
|
||||||
|
FifoSvc: fifoSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,15 +272,20 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
CreatedBy: actorID,
|
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")
|
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 {
|
for _, sourceDetail := range req.SourceKandangs {
|
||||||
productWarehouseId := sourceWarehouseMap[sourceDetail.ProjectFlockKandangId]
|
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")
|
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)
|
targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId)
|
||||||
if err != nil {
|
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")
|
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{
|
target := entity.LayingTransferTarget{
|
||||||
LayingTransferId: createBody.Id,
|
LayingTransferId: createBody.Id,
|
||||||
TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
|
TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
|
||||||
Qty: targetDetail.Quantity,
|
Qty: targetDetail.Quantity,
|
||||||
ProductWarehouseId: &targetWarehouse.Id,
|
ProductWarehouseId: &targetPW.Id,
|
||||||
}
|
}
|
||||||
if err := dbTransaction.Create(&target).Error; err != nil {
|
if err := dbTransaction.Create(&target).Error; err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target")
|
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 {
|
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)
|
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 {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
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 {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
repoTx := s.Repository.WithTx(dbTransaction)
|
||||||
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
||||||
productWarehouseRepoTx := s.ProductWarehouseRepo.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 oldSource.ProductWarehouseId != nil && oldSource.Qty > 0 {
|
||||||
|
|
||||||
if err := productWarehouseRepoTx.PatchOne(c.Context(), *oldSource.ProductWarehouseId, map[string]any{
|
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 {
|
}, nil); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore warehouse quantity")
|
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
|
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,
|
"transfer_date": transferDate,
|
||||||
"notes": req.Reason,
|
"notes": req.Reason,
|
||||||
"pending_usage_qty": &totalSourceQty,
|
"pending_usage_qty": &totalSourceQty,
|
||||||
@@ -466,7 +497,7 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
|||||||
return err
|
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")
|
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")
|
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{
|
target := entity.LayingTransferTarget{
|
||||||
LayingTransferId: id,
|
LayingTransferId: id,
|
||||||
TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
|
TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId,
|
||||||
Qty: targetDetail.Quantity,
|
Qty: targetDetail.Quantity,
|
||||||
ProductWarehouseId: &targetWarehouse.Id,
|
ProductWarehouseId: &targetPW.Id,
|
||||||
}
|
}
|
||||||
if err := dbTransaction.Create(&target).Error; err != nil {
|
if err := dbTransaction.Create(&target).Error; err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target")
|
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 {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
repoTx := s.Repository.WithTx(dbTransaction)
|
||||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||||
|
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
||||||
|
|
||||||
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
||||||
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), id)
|
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 source.ProductWarehouseId != nil && source.Qty > 0 {
|
||||||
|
|
||||||
if err := productWarehouseRepoTx.PatchOne(c.Context(), *source.ProductWarehouseId, map[string]any{
|
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 {
|
}, nil); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore source warehouse quantity")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore source warehouse quantity")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
|
||||||
for _, source := range sources {
|
for _, source := range sources {
|
||||||
populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId)
|
populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
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")
|
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 {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
repoTx := s.Repository.WithTx(dbTransaction)
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
||||||
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
|
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
|
||||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
|
||||||
|
|
||||||
for _, approvableID := range approvableIDs {
|
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 err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("TransferLaying %d not found", approvableID))
|
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 {
|
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)
|
for _, source := range sources {
|
||||||
if err != nil {
|
if source.ProductWarehouseId == nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source warehouse")
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse not found for transfer %d", approvableID))
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to consume FIFO stock for source %d: %v", source.ProductWarehouseId, err))
|
||||||
continue
|
}
|
||||||
}
|
}
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse")
|
|
||||||
|
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(
|
if err := dbTransaction.Model(&entity.LayingTransfer{}).
|
||||||
c.Context(),
|
Where("id = ?", approvableID).
|
||||||
dbTransaction,
|
Updates(map[string]interface{}{
|
||||||
sourceWarehouse.ProductId,
|
"total_qty": replenishResult.AddedQuantity,
|
||||||
targetWarehouse.Id,
|
}).Error; err != nil {
|
||||||
target.Qty,
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update total quantity for transfer")
|
||||||
actorID,
|
|
||||||
); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create or update product warehouse")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -708,9 +750,10 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
|||||||
usageQty := *transfer.PendingUsageQty
|
usageQty := *transfer.PendingUsageQty
|
||||||
updateData := map[string]any{
|
updateData := map[string]any{
|
||||||
"usage_qty": usageQty,
|
"usage_qty": usageQty,
|
||||||
|
"total_qty": usageQty, // Same as usage_qty for initial transfer
|
||||||
"pending_usage_qty": nil,
|
"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")
|
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
|
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)
|
productWarehouseRepoTx := rInventory.NewProductWarehouseRepository(tx)
|
||||||
|
|
||||||
existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
|
existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
|
||||||
if err == nil && existing != nil {
|
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 nil, err
|
||||||
}
|
}
|
||||||
return existing, nil
|
return existing, nil
|
||||||
@@ -775,9 +818,10 @@ func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
newWarehouse := &entity.ProductWarehouse{
|
newWarehouse := &entity.ProductWarehouse{
|
||||||
ProductId: productID,
|
ProductId: productID,
|
||||||
WarehouseId: warehouseID,
|
WarehouseId: warehouseID,
|
||||||
Quantity: quantity,
|
ProjectFlockKandangId: projectFlockKandangId, // Set flock ID agar bisa di-chickin di target flock
|
||||||
|
Quantity: quantity,
|
||||||
// CreatedBy: actorID,
|
// 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 := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||||
_ = fifoService.RegisterStockable(fifo.StockableConfig{
|
_ = fifoService.RegisterStockable(fifo.StockableConfig{
|
||||||
Key: fifo.StockableKey("PURCHASE_ITEMS"),
|
Key: fifo.StockableKeyPurchaseItems,
|
||||||
Table: "purchase_items",
|
Table: "purchase_items",
|
||||||
Columns: fifo.StockableColumns{
|
Columns: fifo.StockableColumns{
|
||||||
ID: "id",
|
ID: "id",
|
||||||
|
|||||||
@@ -42,8 +42,7 @@ type PurchaseService interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
priceTolerance = 0.0001
|
priceTolerance = 0.0001
|
||||||
purchaseStockableKey = fifo.StockableKey("PURCHASE_ITEMS")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type purchaseService struct {
|
type purchaseService struct {
|
||||||
@@ -924,7 +923,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
if _, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{
|
||||||
StockableKey: purchaseStockableKey,
|
StockableKey: fifo.StockableKeyPurchaseItems,
|
||||||
StockableID: adj.itemID,
|
StockableID: adj.itemID,
|
||||||
ProductWarehouseID: adj.pwID,
|
ProductWarehouseID: adj.pwID,
|
||||||
Quantity: adj.qty,
|
Quantity: adj.qty,
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context,
|
|||||||
Where("r.deleted_at IS NULL")
|
Where("r.deleted_at IS NULL")
|
||||||
recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs)
|
recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs)
|
||||||
|
|
||||||
purchaseStockableKey := fifo.StockableKey("PURCHASE_ITEMS").String()
|
purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
|
||||||
transferStockableKey := fifo.StockableKey("STOCK_TRANSFER_DETAILS").String()
|
transferStockableKey := fifo.StockableKey("STOCK_TRANSFER_DETAILS").String()
|
||||||
|
|
||||||
query := r.db.WithContext(ctx).
|
query := r.db.WithContext(ctx).
|
||||||
|
|||||||
@@ -1,7 +1,18 @@
|
|||||||
package fifo
|
package fifo
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
// Usable Keys
|
||||||
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
|
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
||||||
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
|
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).
|
// UsableConfig registers a table that consumes stock (recordings, adjustments, sales, etc).
|
||||||
type UsableConfig struct {
|
type UsableConfig struct {
|
||||||
Key UsableKey
|
Key UsableKey
|
||||||
Table string
|
Table string
|
||||||
Columns UsableColumns
|
Columns UsableColumns
|
||||||
OrderBy []string
|
OrderBy []string
|
||||||
Scope QueryScope
|
Scope QueryScope
|
||||||
|
ExcludedStockables []StockableKey // Stockables to exclude when consuming stock
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
Reference in New Issue
Block a user