mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'dev/teguh' into 'feat/BE/Sprint-8'
Feat[BE US# master data]: create standard production master data and adjust fifo stock module and document module on some main module See merge request mbugroup/lti-api!109
This commit is contained in:
@@ -505,12 +505,25 @@ func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWa
|
||||
|
||||
var lots []stockLot
|
||||
for key, cfg := range configs {
|
||||
selectStmt := fmt.Sprintf(
|
||||
"%s AS id, %s AS available_qty, %s AS created_at",
|
||||
cfg.Columns.ID,
|
||||
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
|
||||
cfg.Columns.CreatedAt,
|
||||
)
|
||||
|
||||
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
|
||||
|
||||
var selectStmt string
|
||||
if usesNumericTime {
|
||||
|
||||
selectStmt = fmt.Sprintf(
|
||||
"%s AS id, %s AS available_qty, '1970-01-01 00:00:00 UTC'::timestamp AS created_at",
|
||||
cfg.Columns.ID,
|
||||
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
|
||||
)
|
||||
} else {
|
||||
selectStmt = fmt.Sprintf(
|
||||
"%s AS id, %s AS available_qty, %s AS created_at",
|
||||
cfg.Columns.ID,
|
||||
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
|
||||
cfg.Columns.CreatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
var rows []struct {
|
||||
ID uint
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- Rollback: restore document columns to expenses table
|
||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS document_path JSON;
|
||||
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS realization_document_path JSON;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- Delete document columns from expenses table since we now use Document service with polymorphic relations
|
||||
ALTER TABLE expenses DROP COLUMN IF EXISTS document_path;
|
||||
ALTER TABLE expenses DROP COLUMN IF EXISTS realization_document_path;
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
-- ============================================
|
||||
-- Rollback: Remove FIFO fields and restore qty column
|
||||
-- ============================================
|
||||
|
||||
-- STEP 1: Drop indexes
|
||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_fifo_lookup;
|
||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_pending_qty;
|
||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_usage_qty;
|
||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_created_at;
|
||||
|
||||
-- STEP 2: Drop constraints
|
||||
ALTER TABLE marketing_delivery_products
|
||||
DROP CONSTRAINT IF EXISTS chk_marketing_delivery_products_fifo_nonneg;
|
||||
|
||||
-- STEP 3: Restore qty column from usage_qty data
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) DEFAULT 0 NOT NULL;
|
||||
|
||||
-- Migrate data back from usage_qty to qty
|
||||
UPDATE marketing_delivery_products
|
||||
SET qty = usage_qty
|
||||
WHERE qty = 0;
|
||||
|
||||
-- STEP 4: Drop FIFO columns
|
||||
ALTER TABLE marketing_delivery_products
|
||||
DROP COLUMN IF EXISTS usage_qty,
|
||||
DROP COLUMN IF EXISTS pending_qty,
|
||||
DROP COLUMN IF EXISTS created_at;
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
-- ============================================
|
||||
-- Add FIFO fields to marketing_delivery_products
|
||||
-- This migration adds fields needed for FIFO stock management
|
||||
-- and removes the old qty field in favor of FIFO-based allocation
|
||||
-- ============================================
|
||||
|
||||
-- STEP 0: Drop orphan indexes from previous migration
|
||||
DROP INDEX IF EXISTS idx_marketing_delivery_products_deleted_at;
|
||||
|
||||
-- STEP 1: Add created_at column (required for FIFO ordering)
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
|
||||
|
||||
-- STEP 2: Add FIFO tracking fields
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ADD COLUMN IF NOT EXISTS usage_qty NUMERIC(15, 3) DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS pending_qty NUMERIC(15, 3) DEFAULT 0;
|
||||
|
||||
-- STEP 3: Migrate data from old qty to usage_qty for existing records
|
||||
-- This preserves existing quantity data as allocated quantity
|
||||
UPDATE marketing_delivery_products
|
||||
SET
|
||||
usage_qty = COALESCE(qty, 0),
|
||||
pending_qty = 0
|
||||
WHERE usage_qty = 0;
|
||||
|
||||
-- STEP 4: Drop the old qty column (replaced by usage_qty + pending_qty)
|
||||
ALTER TABLE marketing_delivery_products
|
||||
DROP COLUMN IF EXISTS qty;
|
||||
|
||||
-- STEP 5: Make FIFO fields NOT NULL
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ALTER COLUMN usage_qty SET NOT NULL,
|
||||
ALTER COLUMN pending_qty SET NOT NULL,
|
||||
ALTER COLUMN created_at SET NOT NULL;
|
||||
|
||||
-- STEP 6: Add constraints to ensure non-negative values
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ADD CONSTRAINT chk_marketing_delivery_products_fifo_nonneg CHECK (
|
||||
usage_qty >= 0 AND
|
||||
pending_qty >= 0
|
||||
);
|
||||
|
||||
-- STEP 7: Create indexes for FIFO operations
|
||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_created_at
|
||||
ON marketing_delivery_products(created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_usage_qty
|
||||
ON marketing_delivery_products(usage_qty)
|
||||
WHERE usage_qty > 0;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_pending_qty
|
||||
ON marketing_delivery_products(pending_qty)
|
||||
WHERE pending_qty > 0;
|
||||
|
||||
-- Composite index for FIFO lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_marketing_delivery_products_fifo_lookup
|
||||
ON marketing_delivery_products(marketing_product_id, created_at DESC);
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
-- Remove foreign key constraint
|
||||
ALTER TABLE marketing_delivery_products
|
||||
DROP CONSTRAINT IF EXISTS fk_marketing_delivery_products_product_warehouse;
|
||||
|
||||
-- Drop product_warehouse_id column
|
||||
ALTER TABLE marketing_delivery_products
|
||||
DROP COLUMN IF EXISTS product_warehouse_id;
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
-- Add product_warehouse_id column to marketing_delivery_products
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ADD COLUMN IF NOT EXISTS product_warehouse_id INT NOT NULL DEFAULT 0;
|
||||
|
||||
-- Fill product_warehouse_id from marketing_products
|
||||
UPDATE marketing_delivery_products mdp
|
||||
SET product_warehouse_id = mp.product_warehouse_id
|
||||
FROM marketing_products mp
|
||||
WHERE mdp.marketing_product_id = mp.id
|
||||
AND mdp.product_warehouse_id = 0;
|
||||
|
||||
-- Set NOT NULL constraint
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ALTER COLUMN product_warehouse_id SET NOT NULL;
|
||||
|
||||
-- Add foreign key constraint
|
||||
ALTER TABLE marketing_delivery_products
|
||||
ADD CONSTRAINT fk_marketing_delivery_products_product_warehouse
|
||||
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id);
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
-- Drop indexes
|
||||
DROP INDEX IF EXISTS idx_standard_growth_details_standard_week;
|
||||
DROP INDEX IF EXISTS idx_production_standard_details_standard_week;
|
||||
DROP INDEX IF EXISTS idx_production_standards_project_category;
|
||||
DROP INDEX IF EXISTS idx_production_standards_deleted_at;
|
||||
|
||||
-- Drop tables (in reverse order due to foreign keys)
|
||||
DROP TABLE IF EXISTS standard_growth_details;
|
||||
DROP TABLE IF EXISTS production_standard_details;
|
||||
DROP TABLE IF EXISTS production_standards;
|
||||
@@ -0,0 +1,96 @@
|
||||
-- Create production_standards table
|
||||
CREATE TABLE IF NOT EXISTS production_standards (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
project_category VARCHAR(20) NOT NULL CHECK (project_category IN ('GROWING', 'LAYING')),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT
|
||||
);
|
||||
|
||||
-- Create index for deleted_at (soft delete)
|
||||
CREATE INDEX idx_production_standards_deleted_at ON production_standards(deleted_at);
|
||||
|
||||
-- Tambahkan Foreign Key ke users
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||
ALTER TABLE production_standards
|
||||
ADD CONSTRAINT fk_production_standards_created_by
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Index
|
||||
CREATE INDEX idx_production_standards_created_by ON production_standards(created_by);
|
||||
|
||||
-- Create production_standard_details table
|
||||
CREATE TABLE IF NOT EXISTS production_standard_details (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
production_standard_id BIGINT NOT NULL,
|
||||
week INT NOT NULL,
|
||||
target_hen_day_production NUMERIC(15, 3),
|
||||
target_hen_house_production NUMERIC(15, 3),
|
||||
target_egg_weight NUMERIC(15, 3),
|
||||
target_egg_mass NUMERIC(15, 3),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Tambahkan Foreign Key ke production_standards
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'production_standards') THEN
|
||||
ALTER TABLE production_standard_details
|
||||
ADD CONSTRAINT fk_production_standard_details_standard
|
||||
FOREIGN KEY (production_standard_id) REFERENCES production_standards(id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Create unique constraint for standard_id + week
|
||||
CREATE UNIQUE INDEX idx_production_standard_details_standard_week
|
||||
ON production_standard_details(production_standard_id, week);
|
||||
|
||||
-- Create standard_growth_details table
|
||||
CREATE TABLE IF NOT EXISTS standard_growth_details (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
production_standard_id BIGINT NOT NULL,
|
||||
target_mean_bw NUMERIC(15, 3),
|
||||
max_depletion NUMERIC(15, 3),
|
||||
min_uniformity NUMERIC(15, 3) NOT NULL,
|
||||
week INT NOT NULL,
|
||||
feed_intake NUMERIC(15, 3),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
created_by BIGINT
|
||||
);
|
||||
|
||||
-- Tambahkan Foreign Key ke production_standards
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'production_standards') THEN
|
||||
ALTER TABLE standard_growth_details
|
||||
ADD CONSTRAINT fk_standard_growth_details_standard
|
||||
FOREIGN KEY (production_standard_id) REFERENCES production_standards(id) ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Tambahkan Foreign Key ke users
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||
ALTER TABLE standard_growth_details
|
||||
ADD CONSTRAINT fk_standard_growth_details_created_by
|
||||
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Create unique constraint for standard_id + week
|
||||
CREATE UNIQUE INDEX idx_standard_growth_details_standard_week
|
||||
ON standard_growth_details(production_standard_id, week);
|
||||
|
||||
-- Index
|
||||
CREATE INDEX idx_standard_growth_details_created_by ON standard_growth_details(created_by);
|
||||
|
||||
-- Create index for project_category
|
||||
CREATE INDEX idx_production_standards_project_category ON production_standards(project_category);
|
||||
@@ -1,7 +1,6 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -13,8 +12,6 @@ type Expense struct {
|
||||
SupplierId uint64 `gorm:""`
|
||||
Category string `gorm:"type:varchar(50);not null"`
|
||||
PoNumber string `gorm:"type:varchar(50)"`
|
||||
DocumentPath sql.NullString `gorm:"type:json"`
|
||||
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"`
|
||||
RealizationDate time.Time `gorm:"type:date;column:realization_date"`
|
||||
TransactionDate time.Time `gorm:"type:date;not null"`
|
||||
Notes string `gorm:"type:text;column:notes"`
|
||||
@@ -23,8 +20,10 @@ type Expense struct {
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
|
||||
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
|
||||
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
|
||||
Documents []Document `gorm:"foreignKey:DocumentableId;references:Id"`
|
||||
RealizationDocuments []Document `gorm:"foreignKey:DocumentableId;references:Id"`
|
||||
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
|
||||
}
|
||||
|
||||
@@ -5,15 +5,20 @@ import (
|
||||
)
|
||||
|
||||
type MarketingDeliveryProduct struct {
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
MarketingProductId uint `gorm:"uniqueIndex;not null"`
|
||||
Qty float64 `gorm:"type:numeric(15,3)"`
|
||||
UnitPrice float64 `gorm:"type:numeric(15,3)"`
|
||||
TotalWeight float64 `gorm:"type:numeric(15,3)"`
|
||||
AvgWeight float64 `gorm:"type:numeric(15,3)"`
|
||||
TotalPrice float64 `gorm:"type:numeric(15,3)"`
|
||||
DeliveryDate *time.Time `gorm:"type:timestamptz"`
|
||||
VehicleNumber string `gorm:"type:varchar(50)"`
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
MarketingProductId uint `gorm:"uniqueIndex;not null"`
|
||||
ProductWarehouseId uint `gorm:"not null"`
|
||||
UnitPrice float64 `gorm:"type:numeric(15,3)"`
|
||||
TotalWeight float64 `gorm:"type:numeric(15,3)"`
|
||||
AvgWeight float64 `gorm:"type:numeric(15,3)"`
|
||||
TotalPrice float64 `gorm:"type:numeric(15,3)"`
|
||||
DeliveryDate *time.Time `gorm:"type:timestamptz"`
|
||||
VehicleNumber string `gorm:"type:varchar(50)"`
|
||||
|
||||
// FIFO Fields
|
||||
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
|
||||
PendingQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
|
||||
CreatedAt *time.Time `gorm:"type:timestamptz;not null"`
|
||||
|
||||
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProductionStandard struct {
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
Name string `gorm:"type:varchar(100);uniqueIndex;not null"`
|
||||
ProjectCategory string `gorm:"type:varchar(20);not null"`
|
||||
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"type:timestamptz;not null"`
|
||||
DeletedAt *time.Time `gorm:"type:timestamptz"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
ProductionStandardDetails []ProductionStandardDetail `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
||||
StandardGrowthDetails []StandardGrowthDetail `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ProductionStandardDetail struct {
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
ProductionStandardId uint `gorm:"not null"`
|
||||
Week int `gorm:"not null"`
|
||||
TargetHenDayProduction *float64 `gorm:"type:numeric(15,3)"`
|
||||
TargetHenHouseProduction *float64 `gorm:"type:numeric(15,3)"`
|
||||
TargetEggWeight *float64 `gorm:"type:numeric(15,3)"`
|
||||
TargetEggMass *float64 `gorm:"type:numeric(15,3)"`
|
||||
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
|
||||
UpdatedAt time.Time `gorm:"type:timestamptz;not null"`
|
||||
|
||||
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type StandardGrowthDetail struct {
|
||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||
ProductionStandardId uint `gorm:"not null"`
|
||||
TargetMeanBw *float64 `gorm:"type:numeric(15,3)"`
|
||||
MaxDepletion *float64 `gorm:"type:numeric(15,3)"`
|
||||
MinUniformity float64 `gorm:"type:numeric(15,3);not null"`
|
||||
Week int `gorm:"not null"`
|
||||
FeedIntake *float64 `gorm:"type:numeric(15,3)"`
|
||||
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
|
||||
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
|
||||
}
|
||||
@@ -20,4 +20,5 @@ type StockTransfer struct {
|
||||
Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"`
|
||||
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy"`
|
||||
Documents []Document `gorm:"foreignKey:DocumentableId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
}
|
||||
|
||||
@@ -2,16 +2,6 @@ package entities
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
LogTypeAdjustment = "ADJUSTMENT"
|
||||
LogTypeTransfer = "TRANSFER"
|
||||
)
|
||||
|
||||
const (
|
||||
TransactionTypeIncrease = "INCREASE"
|
||||
TransactionTypeDecrease = "DECREASE"
|
||||
)
|
||||
|
||||
type StockLog struct {
|
||||
Id uint `gorm:"primaryKey;column:id"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null;index"`
|
||||
|
||||
@@ -4,20 +4,21 @@ import "time"
|
||||
|
||||
// DETAIL EKSPEDISI
|
||||
type StockTransferDelivery struct {
|
||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||
StockTransferId uint64
|
||||
SupplierId uint64
|
||||
VehiclePlate string
|
||||
DriverName string
|
||||
DocumentNumber string
|
||||
DocumentPath string
|
||||
ShippingCostItem float64
|
||||
ShippingCostTotal float64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time `gorm:"index"`
|
||||
// Relations
|
||||
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
||||
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
|
||||
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
|
||||
}
|
||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||
StockTransferId uint64
|
||||
SupplierId uint64
|
||||
VehiclePlate string
|
||||
DriverName string
|
||||
DocumentNumber string
|
||||
DocumentPath string
|
||||
ShippingCostItem float64
|
||||
ShippingCostTotal float64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time `gorm:"index"`
|
||||
// Relations
|
||||
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
||||
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
|
||||
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
|
||||
Documents []Document `gorm:"foreignKey:DocumentableId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ const (
|
||||
P_DeliveryGetAll = "lti.marketing.delivery_order.list"
|
||||
P_DeliveryGetOne = "lti.marketing.delivery_order.detail"
|
||||
P_DeliveryUpdateOne = "lti.marketing.delivery_order.update"
|
||||
P_DeliveryCreateOne = "lti.marketing.delivery_order.Create"
|
||||
P_SalesOrderDelete = "lti.marketing.sales_order.delete"
|
||||
P_SalesOrderApproval = "lti.marketing.sales_order.approve"
|
||||
P_SalesOrderCreateOne = "lti.marketing.sales_order.create"
|
||||
|
||||
@@ -64,7 +64,7 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
||||
DoNumber: doNumber,
|
||||
Product: product,
|
||||
Customer: customer,
|
||||
Qty: e.Qty,
|
||||
Qty: e.UsageQty, // Show allocated quantity from FIFO
|
||||
Weight: e.TotalWeight,
|
||||
AvgWeight: e.AvgWeight,
|
||||
Price: e.UnitPrice,
|
||||
|
||||
@@ -783,7 +783,7 @@ func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow)
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeAdjustment, false)
|
||||
rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeAdjustment), false)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -792,7 +792,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
|
||||
}
|
||||
|
||||
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
|
||||
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeTransfer, true)
|
||||
rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeTransfer), true)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -712,13 +712,13 @@ func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlo
|
||||
)
|
||||
|
||||
for _, product := range deliveryProducts {
|
||||
if product.Qty == 0 {
|
||||
if product.UsageQty == 0 {
|
||||
continue
|
||||
}
|
||||
projectFlockKandang := product.MarketingProduct.ProductWarehouse.ProjectFlockKandang
|
||||
ageWeeks := dto.CalculateAgeFromChickinDataProduksi(projectFlockKandang, product.DeliveryDate)
|
||||
totalAgeWeeks += float64(ageWeeks) * product.Qty
|
||||
totalQty += product.Qty
|
||||
totalAgeWeeks += float64(ageWeeks) * product.UsageQty
|
||||
totalQty += product.UsageQty
|
||||
}
|
||||
|
||||
if totalQty == 0 {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -41,8 +40,8 @@ type ExpenseListDTO struct {
|
||||
|
||||
type ExpenseDetailDTO struct {
|
||||
ExpenseBaseDTO
|
||||
Documents []DocumentDTO `json:"documents,omitempty"`
|
||||
RealizationDocs []DocumentDTO `json:"realization_docs,omitempty"`
|
||||
Documents []DocumentDTO `json:"documents"`
|
||||
RealizationDocs []DocumentDTO `json:"realization_docs"`
|
||||
Kandangs []KandangGroupDTO `json:"kandangs,omitempty"`
|
||||
TotalPengajuan float64 `json:"total_pengajuan"`
|
||||
TotalRealisasi float64 `json:"total_realisasi"`
|
||||
@@ -179,12 +178,20 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
|
||||
var pengajuans []ExpenseNonstockDTO
|
||||
var realisasi []ExpenseRealizationDTO
|
||||
|
||||
if e.DocumentPath.Valid && e.DocumentPath.String != "" {
|
||||
json.Unmarshal([]byte(e.DocumentPath.String), &documents)
|
||||
// Map documents from Document service
|
||||
for _, doc := range e.Documents {
|
||||
documents = append(documents, DocumentDTO{
|
||||
ID: uint64(doc.Id),
|
||||
Path: doc.Path,
|
||||
})
|
||||
}
|
||||
|
||||
if e.RealizationDocumentPath.Valid && e.RealizationDocumentPath.String != "" {
|
||||
json.Unmarshal([]byte(e.RealizationDocumentPath.String), &realizationDocs)
|
||||
// Map realization documents from Document service
|
||||
for _, doc := range e.RealizationDocuments {
|
||||
realizationDocs = append(realizationDocs, DocumentDTO{
|
||||
ID: uint64(doc.Id),
|
||||
Path: doc.Path,
|
||||
})
|
||||
}
|
||||
|
||||
if len(e.Nonstocks) > 0 {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package expenses
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
@@ -31,15 +32,20 @@ func (ExpenseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
realizationRepo := rExpense.NewExpenseRealizationRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||
documentRepo := commonRepo.NewDocumentRepository(db)
|
||||
|
||||
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create document service: %v", err))
|
||||
}
|
||||
|
||||
// Register workflow steps for EXPENSES approval
|
||||
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register expense approval workflow: %v", err))
|
||||
}
|
||||
|
||||
expenseService := sExpense.NewExpenseService(expenseRepo, supplierRepo, nonstockRepo, approvalSvc, realizationRepo, projectFlockKandangRepo, validate)
|
||||
expenseService := sExpense.NewExpenseService(expenseRepo, supplierRepo, nonstockRepo, approvalSvc, realizationRepo, projectFlockKandangRepo, documentSvc, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ExpenseRoutes(router, userService, expenseService)
|
||||
|
||||
@@ -2,11 +2,8 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
@@ -49,9 +46,10 @@ type expenseService struct {
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
RealizationRepository repository.ExpenseRealizationRepository
|
||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||
DocumentSvc commonSvc.DocumentService
|
||||
}
|
||||
|
||||
func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierRepo.SupplierRepository, nonstockRepo nonstockRepo.NonstockRepository, approvalSvc commonSvc.ApprovalService, realizationRepo repository.ExpenseRealizationRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) ExpenseService {
|
||||
func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierRepo.SupplierRepository, nonstockRepo nonstockRepo.NonstockRepository, approvalSvc commonSvc.ApprovalService, realizationRepo repository.ExpenseRealizationRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, validate *validator.Validate) ExpenseService {
|
||||
return &expenseService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
@@ -61,6 +59,7 @@ func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierR
|
||||
ApprovalSvc: approvalSvc,
|
||||
RealizationRepository: realizationRepo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
DocumentSvc: documentSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +71,13 @@ func (s expenseService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
Preload("Nonstocks.Realization").
|
||||
Preload("Nonstocks.ProjectFlockKandang.Kandang.Location").
|
||||
Preload("Nonstocks.Kandang").
|
||||
Preload("Nonstocks.Kandang.Location")
|
||||
Preload("Nonstocks.Kandang.Location").
|
||||
Preload("Documents", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("documentable_type = ?", string(utils.DocumentableTypeExpense))
|
||||
}).
|
||||
Preload("RealizationDocuments", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("documentable_type = ?", string(utils.DocumentableTypeExpenseRealization))
|
||||
})
|
||||
}
|
||||
|
||||
func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expenseDto.ExpenseListDTO, int64, error) {
|
||||
@@ -269,9 +274,23 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create initial approval")
|
||||
}
|
||||
|
||||
if len(req.Documents) > 0 {
|
||||
if err := s.processDocuments(c, expenseRepoTx, uint(expense.Id), req.Documents, false); err != nil {
|
||||
return err
|
||||
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
||||
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
|
||||
for idx, file := range req.Documents {
|
||||
documentFiles = append(documentFiles, commonSvc.DocumentFile{
|
||||
File: file,
|
||||
Type: string(utils.DocumentTypeExpense),
|
||||
Index: &idx,
|
||||
})
|
||||
}
|
||||
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
||||
DocumentableType: string(utils.DocumentableTypeExpense),
|
||||
DocumentableID: expense.Id,
|
||||
CreatedBy: &createdByUint,
|
||||
Files: documentFiles,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense documents")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,9 +546,23 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Documents) > 0 {
|
||||
if err := s.processDocuments(c, expenseRepoTx, id, req.Documents, false); err != nil {
|
||||
return err
|
||||
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
||||
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
|
||||
for idx, file := range req.Documents {
|
||||
documentFiles = append(documentFiles, commonSvc.DocumentFile{
|
||||
File: file,
|
||||
Type: string(utils.DocumentTypeExpense),
|
||||
Index: &idx,
|
||||
})
|
||||
}
|
||||
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
||||
DocumentableType: string(utils.DocumentableTypeExpense),
|
||||
DocumentableID: uint64(id),
|
||||
CreatedBy: &actorID,
|
||||
Files: documentFiles,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense documents")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,9 +691,24 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
|
||||
}
|
||||
|
||||
if len(req.Documents) > 0 {
|
||||
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
|
||||
return err
|
||||
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
||||
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
|
||||
for idx, file := range req.Documents {
|
||||
documentFiles = append(documentFiles, commonSvc.DocumentFile{
|
||||
File: file,
|
||||
Type: string(utils.DocumentTypeExpenseRealization),
|
||||
Index: &idx,
|
||||
})
|
||||
}
|
||||
actorID := uint(1) // TODO: replace with authenticated user id
|
||||
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
||||
DocumentableType: string(utils.DocumentableTypeExpenseRealization),
|
||||
DocumentableID: uint64(expenseID),
|
||||
CreatedBy: &actorID,
|
||||
Files: documentFiles,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense realization documents")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -833,9 +881,24 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Documents) > 0 {
|
||||
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
|
||||
return err
|
||||
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
||||
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
|
||||
for idx, file := range req.Documents {
|
||||
documentFiles = append(documentFiles, commonSvc.DocumentFile{
|
||||
File: file,
|
||||
Type: string(utils.DocumentTypeExpenseRealization),
|
||||
Index: &idx,
|
||||
})
|
||||
}
|
||||
actorID := uint(1) // TODO: replace with authenticated user id
|
||||
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
||||
DocumentableType: string(utils.DocumentableTypeExpenseRealization),
|
||||
DocumentableID: uint64(expenseID),
|
||||
CreatedBy: &actorID,
|
||||
Files: documentFiles,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense realization documents")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -870,79 +933,6 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
||||
return responseDTO, nil
|
||||
}
|
||||
|
||||
func (s *expenseService) processDocuments(ctx *fiber.Ctx, expenseRepoTx repository.ExpenseRepository, expenseID uint, documents []*multipart.FileHeader, isRealization bool) error {
|
||||
|
||||
if len(documents) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var existingDocuments []expenseDto.DocumentDTO
|
||||
var fieldName string
|
||||
|
||||
if isRealization {
|
||||
fieldName = "realization_document_path"
|
||||
} else {
|
||||
fieldName = "document_path"
|
||||
}
|
||||
|
||||
expense, err := expenseRepoTx.GetByID(ctx.Context(), expenseID, nil)
|
||||
if err != nil {
|
||||
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense for document processing")
|
||||
}
|
||||
} else {
|
||||
|
||||
var documentField sql.NullString
|
||||
if isRealization {
|
||||
documentField = expense.RealizationDocumentPath
|
||||
} else {
|
||||
documentField = expense.DocumentPath
|
||||
}
|
||||
|
||||
if documentField.Valid && documentField.String != "" {
|
||||
if err := json.Unmarshal([]byte(documentField.String), &existingDocuments); err != nil {
|
||||
existingDocuments = []expenseDto.DocumentDTO{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var startID uint64 = 1
|
||||
if len(existingDocuments) > 0 {
|
||||
|
||||
maxID := uint64(0)
|
||||
for _, doc := range existingDocuments {
|
||||
if doc.ID > maxID {
|
||||
maxID = doc.ID
|
||||
}
|
||||
}
|
||||
startID = maxID + 1
|
||||
}
|
||||
|
||||
for i, doc := range documents {
|
||||
documentPath := doc.Filename
|
||||
|
||||
document := expenseDto.DocumentDTO{
|
||||
ID: startID + uint64(i),
|
||||
Path: documentPath,
|
||||
}
|
||||
existingDocuments = append(existingDocuments, document)
|
||||
}
|
||||
|
||||
documentJSON, err := json.Marshal(existingDocuments)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
|
||||
}
|
||||
|
||||
if err := expenseRepoTx.PatchOne(ctx.Context(), expenseID, map[string]interface{}{
|
||||
fieldName: string(documentJSON),
|
||||
}, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error {
|
||||
|
||||
if err := commonSvc.EnsureRelations(ctx.Context(),
|
||||
@@ -951,62 +941,40 @@ func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, document
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.Repository.DB().WithContext(ctx.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
expenseRepoTx := repository.NewExpenseRepository(tx)
|
||||
if s.DocumentSvc == nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
|
||||
}
|
||||
|
||||
expense, err := expenseRepoTx.GetByID(ctx.Context(), expenseID, nil)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense for document deletion")
|
||||
// Verify document exists and belongs to the expense
|
||||
var documentableType string
|
||||
if isRealization {
|
||||
documentableType = string(utils.DocumentableTypeExpenseRealization)
|
||||
} else {
|
||||
documentableType = string(utils.DocumentableTypeExpense)
|
||||
}
|
||||
|
||||
documents, err := s.DocumentSvc.ListByTarget(ctx.Context(), documentableType, uint64(expenseID))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve documents")
|
||||
}
|
||||
|
||||
documentFound := false
|
||||
var documentIDsToDelete []uint
|
||||
for _, doc := range documents {
|
||||
if uint64(doc.Id) == documentID {
|
||||
documentFound = true
|
||||
documentIDsToDelete = append(documentIDsToDelete, doc.Id)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var existingDocuments []expenseDto.DocumentDTO
|
||||
var fieldName string
|
||||
if !documentFound {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Document not found")
|
||||
}
|
||||
|
||||
if isRealization {
|
||||
fieldName = "realization_document_path"
|
||||
if expense.RealizationDocumentPath.Valid && expense.RealizationDocumentPath.String != "" {
|
||||
if err := json.Unmarshal([]byte(expense.RealizationDocumentPath.String), &existingDocuments); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to parse existing realization documents")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fieldName = "document_path"
|
||||
if expense.DocumentPath.Valid && expense.DocumentPath.String != "" {
|
||||
if err := json.Unmarshal([]byte(expense.DocumentPath.String), &existingDocuments); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to parse existing documents")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var updatedDocuments []expenseDto.DocumentDTO
|
||||
documentFound := false
|
||||
|
||||
for _, doc := range existingDocuments {
|
||||
if doc.ID == documentID {
|
||||
documentFound = true
|
||||
continue
|
||||
}
|
||||
updatedDocuments = append(updatedDocuments, doc)
|
||||
}
|
||||
|
||||
if !documentFound {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Document not found")
|
||||
}
|
||||
|
||||
documentJSON, err := json.Marshal(updatedDocuments)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
|
||||
}
|
||||
|
||||
if err := expenseRepoTx.PatchOne(ctx.Context(), expenseID, map[string]interface{}{
|
||||
fieldName: string(documentJSON),
|
||||
}, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
// Delete document from database and storage
|
||||
if err := s.DocumentSvc.DeleteDocuments(ctx.Context(), documentIDsToDelete, true); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete document")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -70,7 +70,7 @@ func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stockLog.LoggableType != entity.LogTypeAdjustment {
|
||||
if stockLog.LoggableType != string(utils.StockLogTypeAdjustment) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Quantity must be greater than zero")
|
||||
}
|
||||
transactionType := strings.ToUpper(req.TransactionType)
|
||||
if transactionType != entity.TransactionTypeIncrease && transactionType != entity.TransactionTypeDecrease {
|
||||
if transactionType != string(utils.StockLogTransactionTypeIncrease) && transactionType != string(utils.StockLogTransactionTypeDecrease) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction type")
|
||||
}
|
||||
|
||||
@@ -154,14 +154,14 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
||||
|
||||
afterQuantity := productWarehouse.Quantity
|
||||
newLog := &entity.StockLog{
|
||||
// TransactionType: transactionType,
|
||||
LoggableType: entity.LogTypeAdjustment,
|
||||
|
||||
LoggableType: string(utils.StockLogTypeAdjustment),
|
||||
LoggableId: 0,
|
||||
Notes: req.Note,
|
||||
ProductWarehouseId: productWarehouse.Id,
|
||||
CreatedBy: actorID, // TODO: should Get from auth middleware
|
||||
}
|
||||
if transactionType == entity.TransactionTypeIncrease {
|
||||
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
||||
afterQuantity += req.Quantity
|
||||
newLog.Increase = afterQuantity
|
||||
} else {
|
||||
@@ -248,7 +248,7 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
|
||||
|
||||
db = s.withRelations(db)
|
||||
|
||||
db = db.Where("loggable_type = ?", entity.LogTypeAdjustment)
|
||||
db = db.Where("loggable_type = ?", string(utils.StockLogTypeAdjustment))
|
||||
|
||||
if query.TransactionType != "" {
|
||||
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
|
||||
|
||||
@@ -68,7 +68,7 @@ func (u *TransferController) GetOne(c *fiber.Ctx) error {
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get transfer successfully",
|
||||
Data: dto.ToTransferListDTO(*result),
|
||||
Data: dto.ToTransferDetailDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -80,15 +80,19 @@ func (u *TransferController) CreateOne(c *fiber.Ctx) error {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
// ambil file
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
|
||||
}
|
||||
_ = form.File["documents"]
|
||||
// todo: tunggu ada aws baru proses
|
||||
|
||||
result, err := u.TransferService.CreateOne(c, &req)
|
||||
files := form.File["documents"]
|
||||
|
||||
if len(files) != len(req.Deliveries) {
|
||||
return fiber.NewError(fiber.StatusBadRequest,
|
||||
fiber.NewError(fiber.StatusBadRequest, "Jumlah dokumen harus sama dengan jumlah deliveries").Message)
|
||||
}
|
||||
|
||||
result, err := u.TransferService.CreateOne(c, &req, files)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -98,6 +102,6 @@ func (u *TransferController) CreateOne(c *fiber.Ctx) error {
|
||||
Code: fiber.StatusCreated,
|
||||
Status: "success",
|
||||
Message: "Create transfer successfully",
|
||||
Data: dto.ToTransferListDTO(*result),
|
||||
Data: dto.ToTransferDetailDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type TransferRelationDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
TransferReason string `json:"transfer_reason"`
|
||||
@@ -17,7 +15,6 @@ type TransferRelationDTO struct {
|
||||
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
|
||||
}
|
||||
|
||||
// Only id and name for warehouse simple view
|
||||
type WarehouseSimpleDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -43,6 +40,14 @@ type SupplierSimpleDTO struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type DocumentDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Ext string `json:"ext"`
|
||||
Size float64 `json:"size"`
|
||||
}
|
||||
|
||||
type WarehouseDetailDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -65,24 +70,22 @@ type TransferDetailDTO struct {
|
||||
Deliveries []TransferDeliveryDTO `json:"deliveries"`
|
||||
}
|
||||
|
||||
// Detail produk
|
||||
type TransferDetailItemDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
Proudct ProductSimpleDTO `json:"product"`
|
||||
Product ProductSimpleDTO `json:"product"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
}
|
||||
|
||||
// Delivery ekspedisi
|
||||
type TransferDeliveryDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
Supplier SupplierSimpleDTO `json:"supplier"`
|
||||
VehiclePlate string `json:"vehicle_plate"`
|
||||
DriverName string `json:"driver_name"`
|
||||
DocumentNumber string `json:"document_number"`
|
||||
DocumentPath string `json:"document_path"`
|
||||
ShippingCostItem float64 `json:"shipping_cost_item"`
|
||||
ShippingCostTotal float64 `json:"shipping_cost_total"`
|
||||
Items []TransferDeliveryItemDTO `json:"items"`
|
||||
Document *DocumentDTO `json:"document,omitempty"`
|
||||
}
|
||||
|
||||
type TransferDeliveryItemDTO struct {
|
||||
@@ -91,10 +94,7 @@ type TransferDeliveryItemDTO struct {
|
||||
Quantity float64 `json:"quantity"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
|
||||
|
||||
var sourceWarehouse *WarehouseDetailDTO
|
||||
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
|
||||
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
|
||||
@@ -140,7 +140,7 @@ func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
|
||||
Id: w.Id,
|
||||
Name: w.Name,
|
||||
Location: toLocationDTO(w.Location),
|
||||
Area: toAreaDTO(&w.Area), // Ambil area langsung dari warehouse (area_id)
|
||||
Area: toAreaDTO(&w.Area),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,22 +150,21 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
||||
mapped := userDTO.ToUserRelationDTO(*e.CreatedUser)
|
||||
createdUser = &mapped
|
||||
}
|
||||
// Map details
|
||||
|
||||
var details []TransferDetailItemDTO
|
||||
for _, d := range e.Details {
|
||||
details = append(details, TransferDetailItemDTO{
|
||||
Id: d.Id,
|
||||
Proudct: ProductSimpleDTO{
|
||||
Product: ProductSimpleDTO{
|
||||
Id: d.Product.Id,
|
||||
Name: d.Product.Name,
|
||||
},
|
||||
Quantity: d.Quantity,
|
||||
})
|
||||
}
|
||||
// Map deliveries
|
||||
|
||||
var deliveries []TransferDeliveryDTO
|
||||
for _, del := range e.Deliveries {
|
||||
// Map delivery items
|
||||
var items []TransferDeliveryItemDTO
|
||||
for _, item := range del.Items {
|
||||
items = append(items, TransferDeliveryItemDTO{
|
||||
@@ -174,6 +173,19 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
||||
Quantity: item.Quantity,
|
||||
})
|
||||
}
|
||||
|
||||
var document *DocumentDTO
|
||||
if len(del.Documents) > 0 {
|
||||
doc := del.Documents[0] // Take first document
|
||||
document = &DocumentDTO{
|
||||
Id: doc.Id,
|
||||
Path: doc.Path,
|
||||
Name: doc.Name,
|
||||
Ext: doc.Ext,
|
||||
Size: doc.Size,
|
||||
}
|
||||
}
|
||||
|
||||
deliveries = append(deliveries, TransferDeliveryDTO{
|
||||
Id: del.Id,
|
||||
Supplier: SupplierSimpleDTO{
|
||||
@@ -183,12 +195,13 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
||||
VehiclePlate: del.VehiclePlate,
|
||||
DriverName: del.DriverName,
|
||||
DocumentNumber: del.DocumentNumber,
|
||||
DocumentPath: del.DocumentPath,
|
||||
ShippingCostItem: del.ShippingCostItem,
|
||||
ShippingCostTotal: del.ShippingCostTotal,
|
||||
Items: items,
|
||||
Document: document,
|
||||
})
|
||||
}
|
||||
|
||||
return TransferListDTO{
|
||||
TransferRelationDTO: ToTransferRelationDTO(e),
|
||||
CreatedUser: createdUser,
|
||||
@@ -208,21 +221,32 @@ func ToTransferListDTOs(e []entity.StockTransfer) []TransferListDTO {
|
||||
}
|
||||
|
||||
func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
||||
// Map details
|
||||
var details []TransferDetailItemDTO
|
||||
for _, d := range e.Details {
|
||||
details = append(details, TransferDetailItemDTO{
|
||||
Id: d.Id,
|
||||
Proudct: ProductSimpleDTO{
|
||||
Product: ProductSimpleDTO{
|
||||
Id: d.Product.Id,
|
||||
Name: d.Product.Name,
|
||||
},
|
||||
Quantity: d.Quantity,
|
||||
})
|
||||
}
|
||||
// Map deliveries
|
||||
|
||||
var deliveries []TransferDeliveryDTO
|
||||
for _, del := range e.Deliveries {
|
||||
var document *DocumentDTO
|
||||
if len(del.Documents) > 0 {
|
||||
doc := del.Documents[0] // Take first document
|
||||
document = &DocumentDTO{
|
||||
Id: doc.Id,
|
||||
Path: doc.Path,
|
||||
Name: doc.Name,
|
||||
Ext: doc.Ext,
|
||||
Size: doc.Size,
|
||||
}
|
||||
}
|
||||
|
||||
deliveries = append(deliveries, TransferDeliveryDTO{
|
||||
Id: del.Id,
|
||||
Supplier: SupplierSimpleDTO{
|
||||
@@ -232,11 +256,12 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
||||
VehiclePlate: del.VehiclePlate,
|
||||
DriverName: del.DriverName,
|
||||
DocumentNumber: del.DocumentNumber,
|
||||
DocumentPath: del.DocumentPath,
|
||||
ShippingCostItem: del.ShippingCostItem,
|
||||
ShippingCostTotal: del.ShippingCostTotal,
|
||||
Document: document,
|
||||
})
|
||||
}
|
||||
|
||||
return TransferDetailDTO{
|
||||
TransferListDTO: ToTransferListDTO(e),
|
||||
Details: details,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package transfers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
||||
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
|
||||
@@ -29,8 +33,14 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||
documentRepo := commonRepo.NewDocumentRepository(db)
|
||||
|
||||
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo)
|
||||
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, documentSvc)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
TransferRoutes(router, userService, transferService)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
@@ -27,7 +28,7 @@ import (
|
||||
type TransferService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.StockTransfer, error)
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error)
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error)
|
||||
}
|
||||
|
||||
type transferService struct {
|
||||
@@ -42,9 +43,10 @@ type transferService struct {
|
||||
SupplierRepo rSupplier.SupplierRepository
|
||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||
DocumentSvc commonSvc.DocumentService
|
||||
}
|
||||
|
||||
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository) TransferService {
|
||||
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService) TransferService {
|
||||
return &transferService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
@@ -57,6 +59,7 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
|
||||
SupplierRepo: supplierRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||
DocumentSvc: documentSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +75,10 @@ func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
Preload("Details").
|
||||
Preload("Details.Product").
|
||||
Preload("Deliveries.Items").
|
||||
Preload("Deliveries.Supplier")
|
||||
Preload("Deliveries.Supplier").
|
||||
Preload("Deliveries.Documents", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("documentable_type = ?", string(utils.DocumentableTypeTransfer))
|
||||
})
|
||||
}
|
||||
|
||||
func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error) {
|
||||
@@ -94,31 +100,31 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
s.Log.Infof("Retrieved %d transfers", len(transfers))
|
||||
|
||||
return transfers, total, nil
|
||||
}
|
||||
|
||||
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
|
||||
var transfer entity.StockTransfer
|
||||
s.Log.Infof("Attempting to get StockTransfer with ID: %d", id)
|
||||
|
||||
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||
return s.withRelations(db)
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Error getting StockTransfer ID %d: %+v", id, err)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to get transfer by ID: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
|
||||
}
|
||||
|
||||
s.Log.Infof("Retrieved transfer: %+v", transfer)
|
||||
if transferPtr != nil {
|
||||
s.Log.Infof("StockTransfer %d has %d documents", transferPtr.Id, len(transferPtr.Documents))
|
||||
}
|
||||
|
||||
return transferPtr, nil
|
||||
}
|
||||
|
||||
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
|
||||
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error) {
|
||||
|
||||
pwIDs := make([]uint, 0, len(req.Products))
|
||||
|
||||
@@ -180,7 +186,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
|
||||
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context())
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get next movement number: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
|
||||
}
|
||||
movementNumber := fmt.Sprintf("PND-MBU-%05d", seqNum)
|
||||
@@ -198,10 +203,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
if err := s.StockTransferRepo.WithTx(tx).CreateOne(c.Context(), entityTransfer, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock transfer: %+v", err)
|
||||
return err
|
||||
}
|
||||
s.Log.Infof("Stock transfer created: %+v", entityTransfer.Id)
|
||||
|
||||
var details []*entity.StockTransferDetail
|
||||
for _, product := range req.Products {
|
||||
@@ -212,10 +215,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
})
|
||||
}
|
||||
if err := s.StockTransferDetailRepo.WithTx(tx).CreateMany(c.Context(), details, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock transfer details: %+v", err)
|
||||
return err
|
||||
}
|
||||
s.Log.Infof("Stock transfer details created for transfer ID: %+v", entityTransfer.Id)
|
||||
|
||||
var deliveries []*entity.StockTransferDelivery
|
||||
for _, delivery := range req.Deliveries {
|
||||
@@ -224,13 +225,11 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
SupplierId: uint64(delivery.SupplierID),
|
||||
VehiclePlate: delivery.VehiclePlate,
|
||||
DriverName: delivery.DriverName,
|
||||
DocumentPath: "https://tourism.gov.in/sites/default/files/2019-04/dummy-pdf_2.pdf",
|
||||
ShippingCostItem: delivery.DeliveryCostPerItem,
|
||||
ShippingCostTotal: delivery.DeliveryCost,
|
||||
})
|
||||
}
|
||||
if err := s.StockTransferDeliveryRepo.WithTx(tx).CreateMany(c.Context(), deliveries, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock transfer deliveries: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -256,38 +255,53 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
}
|
||||
}
|
||||
if err := s.StockTransferDeliveryItemRepo.WithTx(tx).CreateMany(c.Context(), deliveryItems, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock transfer delivery items: %+v", err)
|
||||
return err
|
||||
}
|
||||
s.Log.Infof("Stock transfer delivery items created for transfer ID: %+v", entityTransfer.Id)
|
||||
|
||||
if s.DocumentSvc != nil && len(files) > 0 {
|
||||
|
||||
for idx, file := range files {
|
||||
documentFiles := []commonSvc.DocumentFile{
|
||||
{
|
||||
File: file,
|
||||
Type: string(utils.DocumentTypeTransfer),
|
||||
Index: &idx,
|
||||
},
|
||||
}
|
||||
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
|
||||
DocumentableType: string(utils.DocumentableTypeTransfer),
|
||||
DocumentableID: deliveries[idx].Id,
|
||||
CreatedBy: &actorID,
|
||||
Files: documentFiles,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d", idx+1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, product := range req.Products {
|
||||
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID))
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get source product warehouse: %+v", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source product warehouse")
|
||||
}
|
||||
if sourcePW.Quantity < product.ProductQty {
|
||||
s.Log.Errorf("Insufficient stock in source warehouse for product ID: %+v", product.ProductID)
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock in source warehouse for product ID: %d", product.ProductID))
|
||||
}
|
||||
sourcePW.Quantity -= product.ProductQty
|
||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), sourcePW.Id, sourcePW, nil); err != nil {
|
||||
s.Log.Errorf("Failed to update source product warehouse: %+v", err)
|
||||
return err
|
||||
}
|
||||
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
|
||||
|
||||
decreaseLog := &entity.StockLog{
|
||||
Decrease: product.ProductQty,
|
||||
Notes: "",
|
||||
LoggableType: entity.LogTypeTransfer,
|
||||
LoggableType: string(utils.StockLogTypeTransfer),
|
||||
LoggableId: uint(entityTransfer.Id),
|
||||
ProductWarehouseId: sourcePW.Id,
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), decreaseLog, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock log decrease: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -295,7 +309,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
||||
)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to get destination product warehouse: %+v", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get destination product warehouse")
|
||||
}
|
||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -311,29 +324,24 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
ProjectFlockKandangId: &projectFlockKandangID,
|
||||
}
|
||||
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create destination product warehouse: %+v", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create destination product warehouse")
|
||||
}
|
||||
s.Log.Infof("Destination product warehouse created: %+v", destPW.Id)
|
||||
}
|
||||
|
||||
destPW.Quantity += product.ProductQty
|
||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), destPW.Id, destPW, nil); err != nil {
|
||||
s.Log.Errorf("Failed to update destination product warehouse: %+v", err)
|
||||
return err
|
||||
}
|
||||
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
|
||||
|
||||
increaseLog := &entity.StockLog{
|
||||
Increase: product.ProductQty,
|
||||
LoggableType: entity.LogTypeTransfer,
|
||||
LoggableType: string(utils.StockLogTypeTransfer),
|
||||
LoggableId: uint(entityTransfer.Id),
|
||||
Notes: "",
|
||||
ProductWarehouseId: destPW.Id,
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), increaseLog, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock log increase: %+v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -343,7 +351,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process transfer transaction")
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err))
|
||||
}
|
||||
|
||||
result, err := s.GetOne(c, uint(entityTransfer.Id))
|
||||
@@ -359,7 +367,6 @@ func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, wa
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
||||
}
|
||||
s.Log.Errorf("Failed to get warehouse %d: %+v", warehouseID, err)
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
|
||||
}
|
||||
|
||||
@@ -372,7 +379,6 @@ func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, wa
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
|
||||
}
|
||||
s.Log.Errorf("Failed to get active project flock for kandang %d: %+v", *warehouse.KandangId, err)
|
||||
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingD
|
||||
return MarketingDeliveryProductDTO{
|
||||
Id: e.Id,
|
||||
MarketingProductId: e.MarketingProductId,
|
||||
Qty: e.Qty,
|
||||
Qty: e.UsageQty,
|
||||
UnitPrice: e.UnitPrice,
|
||||
TotalWeight: e.TotalWeight,
|
||||
AvgWeight: e.AvgWeight,
|
||||
|
||||
@@ -2,6 +2,7 @@ package marketing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -13,11 +14,12 @@ import (
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
)
|
||||
|
||||
type MarketingModule struct{}
|
||||
@@ -31,6 +33,28 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
customerRepo := rCustomer.NewCustomerRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
|
||||
// Initialize FIFO service
|
||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
||||
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
||||
|
||||
// Register marketing_delivery_products as FIFO Usable
|
||||
// Note: ProductWarehouseID comes from marketing_products table via preload
|
||||
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
||||
Key: fifo.UsableKeyMarketingDelivery,
|
||||
Table: "marketing_delivery_products",
|
||||
Columns: fifo.UsableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "product_warehouse_id", // Resolved from marketing_products via preload
|
||||
UsageQuantity: "usage_qty",
|
||||
PendingQuantity: "pending_qty",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
}); err != nil {
|
||||
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
panic(fmt.Sprintf("failed to register marketing delivery usable workflow: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize approval service
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||
@@ -42,9 +66,10 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||
|
||||
// Initialize services
|
||||
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc,warehouseRepo,projectFlockKandangRepo, validate)
|
||||
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate)
|
||||
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, warehouseRepo, projectFlockKandangRepo, validate)
|
||||
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, fifoService, validate)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
// Register routes
|
||||
|
||||
@@ -17,6 +17,9 @@ type MarketingDeliveryProductRepository interface {
|
||||
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
||||
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
|
||||
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error)
|
||||
UpdateFifoFields(ctx context.Context, id uint, usageQty, pendingQty float64) error
|
||||
GetUsageQty(ctx context.Context, id uint) (float64, error)
|
||||
ResetFifoFields(ctx context.Context, id uint) error
|
||||
}
|
||||
|
||||
type MarketingDeliveryProductRepositoryImpl struct {
|
||||
@@ -241,3 +244,33 @@ func containsJoin(db *gorm.DB, tableName string) bool {
|
||||
joinSQL := statement.SQL.String()
|
||||
return strings.Contains(joinSQL, "JOIN "+tableName)
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) UpdateFifoFields(ctx context.Context, id uint, usageQty, pendingQty float64) error {
|
||||
return r.DB().WithContext(ctx).
|
||||
Model(&entity.MarketingDeliveryProduct{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"usage_qty": usageQty,
|
||||
"pending_qty": pendingQty,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) GetUsageQty(ctx context.Context, id uint) (float64, error) {
|
||||
var usageQty float64
|
||||
err := r.DB().WithContext(ctx).
|
||||
Model(&entity.MarketingDeliveryProduct{}).
|
||||
Where("id = ?", id).
|
||||
Select("usage_qty").
|
||||
Scan(&usageQty).Error
|
||||
return usageQty, err
|
||||
}
|
||||
|
||||
func (r *MarketingDeliveryProductRepositoryImpl) ResetFifoFields(ctx context.Context, id uint) error {
|
||||
return r.DB().WithContext(ctx).
|
||||
Model(&entity.MarketingDeliveryProduct{}).
|
||||
Where("id = ?", id).
|
||||
Updates(map[string]interface{}{
|
||||
"usage_qty": 0,
|
||||
"pending_qty": 0,
|
||||
}).Error
|
||||
}
|
||||
|
||||
@@ -16,12 +16,15 @@ func RegisterRoutes(router fiber.Router, userService user.UserService, salesOrde
|
||||
route := router.Group("/marketing")
|
||||
route.Use(m.Auth(userService))
|
||||
|
||||
route.Get("/",m.RequirePermissions(m.P_DeliveryGetAll), deliveryOrdersCtrl.GetAll)
|
||||
route.Get("/:id",m.RequirePermissions(m.P_DeliveryGetOne), deliveryOrdersCtrl.GetOne)
|
||||
route.Delete("/:id",m.RequirePermissions(m.P_SalesOrderDelete), salesOrdersCtrl.DeleteOne)
|
||||
route.Get("/", m.RequirePermissions(m.P_DeliveryGetAll), deliveryOrdersCtrl.GetAll)
|
||||
route.Get("/:id", m.RequirePermissions(m.P_DeliveryGetOne), deliveryOrdersCtrl.GetOne)
|
||||
route.Delete("/:id", m.RequirePermissions(m.P_SalesOrderDelete), salesOrdersCtrl.DeleteOne)
|
||||
|
||||
route.Post("/sales-orders",m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne)
|
||||
route.Patch("/sales-orders/:id",m.RequirePermissions(m.P_SalesOrderUpdateOne), salesOrdersCtrl.UpdateOne)
|
||||
route.Post("/sales-orders/approvals",m.RequirePermissions(m.P_SalesOrderApproval), salesOrdersCtrl.Approval)
|
||||
route.Post("/sales-orders", m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne)
|
||||
route.Patch("/sales-orders/:id", m.RequirePermissions(m.P_SalesOrderUpdateOne), salesOrdersCtrl.UpdateOne)
|
||||
route.Post("/sales-orders/approvals", m.RequirePermissions(m.P_SalesOrderApproval), salesOrdersCtrl.Approval)
|
||||
|
||||
route.Post("/delivery-orders", m.RequirePermissions(m.P_DeliveryCreateOne), deliveryOrdersCtrl.CreateOne)
|
||||
route.Patch("/delivery-orders/:id", m.RequirePermissions(m.P_DeliveryUpdateOne), deliveryOrdersCtrl.UpdateOne)
|
||||
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ import (
|
||||
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -30,12 +30,12 @@ type DeliveryOrdersService interface {
|
||||
}
|
||||
|
||||
type deliveryOrdersService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
MarketingRepo marketingRepo.MarketingRepository
|
||||
MarketingProductRepo marketingRepo.MarketingProductRepository
|
||||
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
FifoSvc commonSvc.FifoService
|
||||
}
|
||||
|
||||
func NewDeliveryOrdersService(
|
||||
@@ -43,15 +43,16 @@ func NewDeliveryOrdersService(
|
||||
marketingProductRepo marketingRepo.MarketingProductRepository,
|
||||
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
|
||||
approvalSvc commonSvc.ApprovalService,
|
||||
fifoSvc commonSvc.FifoService,
|
||||
validate *validator.Validate,
|
||||
) DeliveryOrdersService {
|
||||
return &deliveryOrdersService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
MarketingRepo: marketingRepo,
|
||||
MarketingProductRepo: marketingProductRepo,
|
||||
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
FifoSvc: fifoSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +109,6 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get marketings: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
for i := range marketings {
|
||||
@@ -116,7 +116,7 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
|
||||
return db.Preload("ActionUser")
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Warnf("Failed to load approval for marketing %d: %+v", marketings[i].Id, err)
|
||||
continue
|
||||
}
|
||||
marketings[i].LatestApproval = latestApproval
|
||||
}
|
||||
@@ -247,7 +247,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
|
||||
itemDeliveryDate = &parsedDate
|
||||
}
|
||||
|
||||
deliveryProduct.Qty = requestedProduct.Qty
|
||||
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
||||
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
||||
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
|
||||
deliveryProduct.TotalWeight = requestedProduct.TotalWeight
|
||||
@@ -256,7 +256,8 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
|
||||
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
|
||||
|
||||
if requestedProduct.Qty > 0 {
|
||||
if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, requestedProduct.Qty); err != nil {
|
||||
|
||||
if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -354,8 +355,9 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
|
||||
itemDeliveryDate = deliveryProduct.DeliveryDate
|
||||
}
|
||||
|
||||
oldQty := deliveryProduct.Qty
|
||||
deliveryProduct.Qty = requestedProduct.Qty
|
||||
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
|
||||
|
||||
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
||||
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
||||
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
|
||||
deliveryProduct.TotalWeight = requestedProduct.TotalWeight
|
||||
@@ -363,14 +365,18 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
|
||||
deliveryProduct.DeliveryDate = itemDeliveryDate
|
||||
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
|
||||
|
||||
qtyChange := requestedProduct.Qty - oldQty
|
||||
if qtyChange > 0 {
|
||||
if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, qtyChange); err != nil {
|
||||
return err
|
||||
if requestedProduct.Qty != oldRequestedQty {
|
||||
|
||||
if oldRequestedQty > 0 {
|
||||
if err := s.releaseDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if qtyChange < 0 {
|
||||
if err := s.restoreProductWarehouseStock(c.Context(), dbTransaction, foundMarketingProduct, -qtyChange); err != nil {
|
||||
return err
|
||||
|
||||
if requestedProduct.Qty > 0 {
|
||||
if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,50 +399,79 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
|
||||
return s.getMarketingWithDeliveries(c, id)
|
||||
}
|
||||
|
||||
func (s deliveryOrdersService) validateAndReduceProductWarehouse(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyDeliver float64) error {
|
||||
func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, requestedQty float64) error {
|
||||
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
|
||||
}
|
||||
|
||||
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
|
||||
if deliveryProduct == nil || deliveryProduct.Id == 0 {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Delivery product not found")
|
||||
}
|
||||
|
||||
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
||||
UsableKey: fifo.UsableKeyMarketingDelivery,
|
||||
UsableID: deliveryProduct.Id,
|
||||
ProductWarehouseID: marketingProduct.ProductWarehouseId,
|
||||
Quantity: requestedQty,
|
||||
AllowPending: false,
|
||||
Tx: tx,
|
||||
})
|
||||
|
||||
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
|
||||
|
||||
pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found")
|
||||
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
|
||||
pw, err2 := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
|
||||
if err2 != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check product warehouse stock")
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock")
|
||||
|
||||
if pw == nil || pw.Quantity < requestedQty {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. Available: %.2f, Requested: %.2f", func() float64 { if pw != nil { return pw.Quantity } else { return 0 } }(), requestedQty))
|
||||
}
|
||||
|
||||
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, requestedQty, 0); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if pw.Quantity < qtyDeliver {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for warehouse - available: %.2f, requested: %.2f", pw.Quantity, qtyDeliver))
|
||||
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
|
||||
}
|
||||
|
||||
pw.Quantity = pw.Quantity - qtyDeliver
|
||||
if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s deliveryOrdersService) restoreProductWarehouseStock(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyRestore float64) error {
|
||||
func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct) error {
|
||||
if deliveryProduct == nil || deliveryProduct.Id == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
|
||||
}
|
||||
|
||||
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
|
||||
pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
|
||||
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
|
||||
currentUsage, err := deliveryProductRepo.GetUsageQty(ctx, deliveryProduct.Id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found")
|
||||
}
|
||||
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock")
|
||||
currentUsage = 0
|
||||
}
|
||||
|
||||
pw.Quantity = pw.Quantity + qtyRestore
|
||||
if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock")
|
||||
if currentUsage == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
|
||||
UsableKey: fifo.UsableKeyMarketingDelivery,
|
||||
UsableID: deliveryProduct.Id,
|
||||
Tx: tx,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -307,15 +307,17 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
||||
|
||||
if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
|
||||
mdp := &entity.MarketingDeliveryProduct{
|
||||
MarketingProductId: old.Id,
|
||||
Qty: 0,
|
||||
UnitPrice: 0,
|
||||
TotalWeight: 0,
|
||||
AvgWeight: 0,
|
||||
TotalPrice: 0,
|
||||
DeliveryDate: nil,
|
||||
VehicleNumber: rp.VehicleNumber,
|
||||
UsageQty: 0,
|
||||
PendingQty: 0,
|
||||
}
|
||||
if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product")
|
||||
@@ -340,7 +342,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
||||
}
|
||||
if err == nil {
|
||||
|
||||
if deliveryProduct.DeliveryDate != nil || deliveryProduct.Qty > 0 {
|
||||
if deliveryProduct.DeliveryDate != nil || deliveryProduct.UsageQty > 0 || deliveryProduct.PendingQty > 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id))
|
||||
}
|
||||
|
||||
@@ -602,13 +604,14 @@ func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Cont
|
||||
|
||||
marketingDeliveryProduct := &entity.MarketingDeliveryProduct{
|
||||
MarketingProductId: marketingProduct.Id,
|
||||
Qty: 0,
|
||||
UnitPrice: 0,
|
||||
TotalWeight: 0,
|
||||
AvgWeight: 0,
|
||||
TotalPrice: 0,
|
||||
DeliveryDate: nil,
|
||||
VehicleNumber: rp.VehicleNumber,
|
||||
UsageQty: 0,
|
||||
PendingQty: 0,
|
||||
}
|
||||
if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil {
|
||||
return err
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type ProductionStandardController struct {
|
||||
ProductionStandardService service.ProductionStandardService
|
||||
}
|
||||
|
||||
func NewProductionStandardController(productionStandardService service.ProductionStandardService) *ProductionStandardController {
|
||||
return &ProductionStandardController{
|
||||
ProductionStandardService: productionStandardService,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *ProductionStandardController) GetAll(c *fiber.Ctx) error {
|
||||
query := &validation.Query{
|
||||
Page: c.QueryInt("page", 1),
|
||||
Limit: c.QueryInt("limit", 10),
|
||||
Search: c.Query("search", ""),
|
||||
ProjectCategory: c.Query("project_category", ""),
|
||||
}
|
||||
|
||||
if query.Page < 1 || query.Limit < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||
}
|
||||
|
||||
result, totalResults, err := u.ProductionStandardService.GetAll(c, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.SuccessWithPaginate[dto.ProductionStandardListDTO]{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get all productionStandards successfully",
|
||||
Meta: response.Meta{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
|
||||
TotalResults: totalResults,
|
||||
},
|
||||
Data: dto.ToProductionStandardListDTOs(result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ProductionStandardController) GetOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
result, err := u.ProductionStandardService.GetOne(c, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Get productionStandard successfully",
|
||||
Data: dto.ToProductionStandardDetailDTO(*result, result.StandardGrowthDetails, result.ProductionStandardDetails),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ProductionStandardController) CreateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Create)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.ProductionStandardService.CreateOne(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusCreated,
|
||||
Status: "success",
|
||||
Message: "Create productionStandard successfully",
|
||||
Data: dto.ToProductionStandardDetailDTO(*result, result.StandardGrowthDetails, result.ProductionStandardDetails),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ProductionStandardController) UpdateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.Update)
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.ProductionStandardService.UpdateOne(c, req, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Update productionStandard successfully",
|
||||
Data: dto.ToProductionStandardDetailDTO(*result, result.StandardGrowthDetails, result.ProductionStandardDetails),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *ProductionStandardController) DeleteOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||
}
|
||||
|
||||
if err := u.ProductionStandardService.DeleteOne(c, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Common{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Delete productionStandard successfully",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
|
||||
type ProductionStandardListDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ProjectCategory string `json:"project_category"`
|
||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||
}
|
||||
|
||||
type ProductionStandardDetailDTO struct {
|
||||
ProductionStandardListDTO
|
||||
Details []WeeklyProductionStandardDTO `json:"details"`
|
||||
}
|
||||
|
||||
type GrowthStandardDetailDTO struct {
|
||||
Id uint `json:"id"`
|
||||
TargetMeanBW *float64 `json:"target_mean_bw"`
|
||||
MaxDepletion *float64 `json:"max_depletion"`
|
||||
MinUniformity float64 `json:"min_uniformity"`
|
||||
FeedIntake *float64 `json:"feed_intake"`
|
||||
}
|
||||
|
||||
type EggProductionStandardDetailDTO struct {
|
||||
Id uint `json:"id"`
|
||||
TargetHenDayProduction *float64 `json:"target_hen_day_production"`
|
||||
TargetHenHouseProduction *float64 `json:"target_hen_house_production"`
|
||||
TargetEggWeight *float64 `json:"target_egg_weight"`
|
||||
TargetEggMass *float64 `json:"target_egg_mass"`
|
||||
}
|
||||
|
||||
type WeeklyProductionStandardDTO struct {
|
||||
Week int `json:"week"`
|
||||
GrowthStandardDetail GrowthStandardDetailDTO `json:"growth_standard_detail"`
|
||||
EggProductionStandardDetailDTO *EggProductionStandardDetailDTO `json:"egg_production_standard_detail,omitempty"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
func ToProductionStandardListDTO(e entity.ProductionStandard) ProductionStandardListDTO {
|
||||
var createdUser *userDTO.UserRelationDTO
|
||||
if e.CreatedUser.Id != 0 {
|
||||
mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
|
||||
createdUser = &mapped
|
||||
}
|
||||
|
||||
return ProductionStandardListDTO{
|
||||
Id: e.Id,
|
||||
Name: e.Name,
|
||||
ProjectCategory: e.ProjectCategory,
|
||||
CreatedUser: createdUser,
|
||||
}
|
||||
}
|
||||
|
||||
func ToProductionStandardListDTOs(e []entity.ProductionStandard) []ProductionStandardListDTO {
|
||||
result := make([]ProductionStandardListDTO, len(e))
|
||||
for i, r := range e {
|
||||
result[i] = ToProductionStandardListDTO(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToWeeklyProductionStandardDTO(e entity.StandardGrowthDetail) WeeklyProductionStandardDTO {
|
||||
return WeeklyProductionStandardDTO{
|
||||
Week: e.Week,
|
||||
GrowthStandardDetail: GrowthStandardDetailDTO{
|
||||
Id: e.Id,
|
||||
TargetMeanBW: e.TargetMeanBw,
|
||||
MaxDepletion: e.MaxDepletion,
|
||||
MinUniformity: e.MinUniformity,
|
||||
FeedIntake: e.FeedIntake,
|
||||
},
|
||||
EggProductionStandardDetailDTO: nil, // GROWING category - no egg production details
|
||||
}
|
||||
}
|
||||
|
||||
func ToWeeklyProductionStandardDTOWithDetails(growth entity.StandardGrowthDetail, detail entity.ProductionStandardDetail) WeeklyProductionStandardDTO {
|
||||
eggDetail := &EggProductionStandardDetailDTO{
|
||||
Id: detail.Id,
|
||||
TargetHenDayProduction: detail.TargetHenDayProduction,
|
||||
TargetHenHouseProduction: detail.TargetHenHouseProduction,
|
||||
TargetEggWeight: detail.TargetEggWeight,
|
||||
TargetEggMass: detail.TargetEggMass,
|
||||
}
|
||||
|
||||
return WeeklyProductionStandardDTO{
|
||||
Week: growth.Week,
|
||||
GrowthStandardDetail: GrowthStandardDetailDTO{
|
||||
Id: growth.Id,
|
||||
TargetMeanBW: growth.TargetMeanBw,
|
||||
MaxDepletion: growth.MaxDepletion,
|
||||
MinUniformity: growth.MinUniformity,
|
||||
FeedIntake: growth.FeedIntake,
|
||||
},
|
||||
EggProductionStandardDetailDTO: eggDetail, // LAYING category - with egg production details
|
||||
}
|
||||
}
|
||||
|
||||
func ToWeeklyProductionStandardDTOs(e []entity.StandardGrowthDetail) []WeeklyProductionStandardDTO {
|
||||
result := make([]WeeklyProductionStandardDTO, len(e))
|
||||
for i, r := range e {
|
||||
result[i] = ToWeeklyProductionStandardDTO(r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToWeeklyProductionStandardDTOsWithDetails(
|
||||
growthDetails []entity.StandardGrowthDetail,
|
||||
productionStandardDetails []entity.ProductionStandardDetail,
|
||||
) []WeeklyProductionStandardDTO {
|
||||
result := make([]WeeklyProductionStandardDTO, len(growthDetails))
|
||||
|
||||
// Create map for production standard details by week
|
||||
prodDetailMap := make(map[int]entity.ProductionStandardDetail)
|
||||
for _, detail := range productionStandardDetails {
|
||||
prodDetailMap[detail.Week] = detail
|
||||
}
|
||||
|
||||
// Map growth details and combine with production standard details
|
||||
for i, growth := range growthDetails {
|
||||
if prodDetail, exists := prodDetailMap[growth.Week]; exists {
|
||||
result[i] = ToWeeklyProductionStandardDTOWithDetails(growth, prodDetail)
|
||||
} else {
|
||||
result[i] = ToWeeklyProductionStandardDTO(growth)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func ToEggProductionStandardDetailDTO(e entity.ProductionStandardDetail) EggProductionStandardDetailDTO {
|
||||
return EggProductionStandardDetailDTO{
|
||||
TargetHenDayProduction: e.TargetHenDayProduction,
|
||||
TargetHenHouseProduction: e.TargetHenHouseProduction,
|
||||
TargetEggWeight: e.TargetEggWeight,
|
||||
TargetEggMass: e.TargetEggMass,
|
||||
}
|
||||
}
|
||||
|
||||
func ToProductionStandardDetailDTO(
|
||||
standard entity.ProductionStandard,
|
||||
growthDetails []entity.StandardGrowthDetail,
|
||||
productionStandardDetails []entity.ProductionStandardDetail,
|
||||
) ProductionStandardDetailDTO {
|
||||
return ProductionStandardDetailDTO{
|
||||
ProductionStandardListDTO: ToProductionStandardListDTO(standard),
|
||||
Details: ToWeeklyProductionStandardDTOsWithDetails(growthDetails, productionStandardDetails),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package productionstandards
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
||||
sProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
)
|
||||
|
||||
type ProductionStandardModule struct{}
|
||||
|
||||
func (ProductionStandardModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
|
||||
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
|
||||
standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
productionStandardService := sProductionStandard.NewProductionStandardService(
|
||||
productionStandardRepo,
|
||||
productionStandardDetailRepo,
|
||||
standardGrowthDetailRepo,
|
||||
validate,
|
||||
)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ProductionStandardRoutes(router, userService, productionStandardService)
|
||||
}
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductionStandardRepository interface {
|
||||
repository.BaseRepository[entity.ProductionStandard]
|
||||
GetAll(ctx context.Context, offset, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProductionStandard, int64, error)
|
||||
GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.ProductionStandard, error)
|
||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
GetByProjectCategory(ctx context.Context, projectCategory string) ([]entity.ProductionStandard, error)
|
||||
}
|
||||
|
||||
type ProductionStandardRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.ProductionStandard]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewProductionStandardRepository(db *gorm.DB) ProductionStandardRepository {
|
||||
return &ProductionStandardRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductionStandard](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ProductionStandardRepositoryImpl) GetAll(ctx context.Context, offset, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProductionStandard, int64, error) {
|
||||
var standards []entity.ProductionStandard
|
||||
var total int64
|
||||
|
||||
// Build base query
|
||||
q := r.db.WithContext(ctx).Model(&entity.ProductionStandard{})
|
||||
|
||||
// Apply modifier for filters
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
|
||||
// Count total
|
||||
if err := q.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Re-apply modifier and add preloads for Find
|
||||
q = r.db.WithContext(ctx).Model(&entity.ProductionStandard{})
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
q = q.Preload("CreatedUser")
|
||||
|
||||
// Find with offset and limit
|
||||
if err := q.Offset(offset).Limit(limit).Find(&standards).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return standards, total, nil
|
||||
}
|
||||
|
||||
func (r *ProductionStandardRepositoryImpl) GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.ProductionStandard, error) {
|
||||
var standard entity.ProductionStandard
|
||||
|
||||
q := r.db.WithContext(ctx).Model(&entity.ProductionStandard{})
|
||||
|
||||
// Apply modifier
|
||||
if modifier != nil {
|
||||
q = modifier(q)
|
||||
}
|
||||
|
||||
// Ensure CreatedUser is preloaded
|
||||
q = q.Preload("CreatedUser")
|
||||
|
||||
if err := q.First(&standard, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &standard, nil
|
||||
}
|
||||
|
||||
func (r *ProductionStandardRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
||||
return repository.ExistsByName[entity.ProductionStandard](ctx, r.db, name, excludeID)
|
||||
}
|
||||
|
||||
func (r *ProductionStandardRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||
return repository.Exists[entity.ProductionStandard](ctx, r.db, id)
|
||||
}
|
||||
|
||||
func (r *ProductionStandardRepositoryImpl) GetByProjectCategory(ctx context.Context, projectCategory string) ([]entity.ProductionStandard, error) {
|
||||
var standards []entity.ProductionStandard
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("CreatedUser").
|
||||
Where("project_category = ?", projectCategory).
|
||||
Where("deleted_at IS NULL").
|
||||
Find(&standards).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return standards, nil
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductionStandardDetailRepository interface {
|
||||
repository.BaseRepository[entity.ProductionStandardDetail]
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
GetByProductionStandardID(ctx context.Context, productionStandardId uint) ([]entity.ProductionStandardDetail, error)
|
||||
GetByStandardIDAndWeek(ctx context.Context, standardId uint, week int) (*entity.ProductionStandardDetail, error)
|
||||
DeleteByProductionStandardID(ctx context.Context, productionStandardId uint) error
|
||||
}
|
||||
|
||||
type ProductionStandardDetailRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.ProductionStandardDetail]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewProductionStandardDetailRepository(db *gorm.DB) ProductionStandardDetailRepository {
|
||||
return &ProductionStandardDetailRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductionStandardDetail](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ProductionStandardDetailRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||
return repository.Exists[entity.ProductionStandardDetail](ctx, r.db, id)
|
||||
}
|
||||
|
||||
func (r *ProductionStandardDetailRepositoryImpl) GetByProductionStandardID(ctx context.Context, productionStandardId uint) ([]entity.ProductionStandardDetail, error) {
|
||||
var details []entity.ProductionStandardDetail
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("production_standard_id = ?", productionStandardId).
|
||||
Order("week ASC").
|
||||
Find(&details).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return details, nil
|
||||
}
|
||||
|
||||
func (r *ProductionStandardDetailRepositoryImpl) GetByStandardIDAndWeek(ctx context.Context, standardId uint, week int) (*entity.ProductionStandardDetail, error) {
|
||||
var detail entity.ProductionStandardDetail
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("production_standard_id = ?", standardId).
|
||||
Where("week = ?", week).
|
||||
First(&detail).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &detail, nil
|
||||
}
|
||||
|
||||
func (r *ProductionStandardDetailRepositoryImpl) DeleteByProductionStandardID(ctx context.Context, productionStandardId uint) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Where("production_standard_id = ?", productionStandardId).
|
||||
Delete(&entity.ProductionStandardDetail{}).Error
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type StandardGrowthDetailRepository interface {
|
||||
repository.BaseRepository[entity.StandardGrowthDetail]
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
GetByProductionStandardID(ctx context.Context, productionStandardId uint) ([]entity.StandardGrowthDetail, error)
|
||||
GetByStandardIDAndWeek(ctx context.Context, standardId uint, week int) (*entity.StandardGrowthDetail, error)
|
||||
DeleteByProductionStandardID(ctx context.Context, productionStandardId uint) error
|
||||
}
|
||||
|
||||
type StandardGrowthDetailRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.StandardGrowthDetail]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewStandardGrowthDetailRepository(db *gorm.DB) StandardGrowthDetailRepository {
|
||||
return &StandardGrowthDetailRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.StandardGrowthDetail](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *StandardGrowthDetailRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||
return repository.Exists[entity.StandardGrowthDetail](ctx, r.db, id)
|
||||
}
|
||||
|
||||
func (r *StandardGrowthDetailRepositoryImpl) GetByProductionStandardID(ctx context.Context, productionStandardId uint) ([]entity.StandardGrowthDetail, error) {
|
||||
var details []entity.StandardGrowthDetail
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("production_standard_id = ?", productionStandardId).
|
||||
Order("week ASC").
|
||||
Find(&details).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return details, nil
|
||||
}
|
||||
|
||||
func (r *StandardGrowthDetailRepositoryImpl) GetByStandardIDAndWeek(ctx context.Context, standardId uint, week int) (*entity.StandardGrowthDetail, error) {
|
||||
var detail entity.StandardGrowthDetail
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("production_standard_id = ?", standardId).
|
||||
Where("week = ?", week).
|
||||
First(&detail).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &detail, nil
|
||||
}
|
||||
|
||||
func (r *StandardGrowthDetailRepositoryImpl) DeleteByProductionStandardID(ctx context.Context, productionStandardId uint) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Where("production_standard_id = ?", productionStandardId).
|
||||
Delete(&entity.StandardGrowthDetail{}).Error
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package productionstandards
|
||||
|
||||
import (
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/controllers"
|
||||
productionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services"
|
||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func ProductionStandardRoutes(v1 fiber.Router, u user.UserService, s productionStandard.ProductionStandardService) {
|
||||
ctrl := controller.NewProductionStandardController(s)
|
||||
|
||||
route := v1.Group("/production-standards")
|
||||
route.Use(m.Auth(u))
|
||||
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
route.Get("/:id", ctrl.GetOne)
|
||||
route.Patch("/:id", ctrl.UpdateOne)
|
||||
route.Delete("/:id", ctrl.DeleteOne)
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ProductionStandardService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProductionStandard, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProductionStandard, error)
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProductionStandard, error)
|
||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductionStandard, error)
|
||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||
}
|
||||
|
||||
type productionStandardService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
Repository repository.ProductionStandardRepository
|
||||
ProductionStandardDetailRepo repository.ProductionStandardDetailRepository
|
||||
StandardGrowthDetailRepo repository.StandardGrowthDetailRepository
|
||||
}
|
||||
|
||||
func NewProductionStandardService(
|
||||
repo repository.ProductionStandardRepository,
|
||||
productionStandardDetailRepo repository.ProductionStandardDetailRepository,
|
||||
standardGrowthDetailRepo repository.StandardGrowthDetailRepository,
|
||||
validate *validator.Validate,
|
||||
) ProductionStandardService {
|
||||
return &productionStandardService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
Repository: repo,
|
||||
ProductionStandardDetailRepo: productionStandardDetailRepo,
|
||||
StandardGrowthDetailRepo: standardGrowthDetailRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s productionStandardService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("CreatedUser").
|
||||
Preload("ProductionStandardDetails").
|
||||
Preload("StandardGrowthDetails")
|
||||
}
|
||||
|
||||
func (s productionStandardService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductionStandard, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
productionStandards, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
if params.Search != "" {
|
||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
||||
}
|
||||
if params.ProjectCategory != "" {
|
||||
return db.Where("project_category = ?", params.ProjectCategory)
|
||||
}
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get productionStandards: %+v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
return productionStandards, total, nil
|
||||
}
|
||||
|
||||
func (s productionStandardService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductionStandard, error) {
|
||||
productionStandard, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "ProductionStandard not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed get productionStandard by id: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
return productionStandard, nil
|
||||
}
|
||||
|
||||
func (s *productionStandardService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProductionStandard, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nameExists, err := s.Repository.NameExists(c.Context(), req.Name, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nameExists {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Production standard with name '%s' already exists", req.Name))
|
||||
}
|
||||
|
||||
var createdStandard *entity.ProductionStandard
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
standardRepoTx := repository.NewProductionStandardRepository(tx)
|
||||
productionStandardDetailRepoTx := repository.NewProductionStandardDetailRepository(tx)
|
||||
standardGrowthDetailRepoTx := repository.NewStandardGrowthDetailRepository(tx)
|
||||
|
||||
newStandard := &entity.ProductionStandard{
|
||||
Name: req.Name,
|
||||
ProjectCategory: req.ProjectCategory,
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
|
||||
if err := standardRepoTx.CreateOne(c.Context(), newStandard, nil); err != nil {
|
||||
return fmt.Errorf("failed to create production standard: %w", err)
|
||||
}
|
||||
|
||||
for _, detailReq := range req.Details {
|
||||
if detailReq.ProductionStandardUniformityDetails == nil {
|
||||
return fmt.Errorf("production_standard_uniformity_details is required in week %d", detailReq.Week)
|
||||
}
|
||||
|
||||
if req.ProjectCategory == string(utils.ProjectFlockCategoryLaying) {
|
||||
if detailReq.ProductionStandardDetails == nil {
|
||||
return fmt.Errorf("production_standard_details is required for LAYING category in week %d", detailReq.Week)
|
||||
}
|
||||
|
||||
productionStandardDetail := &entity.ProductionStandardDetail{
|
||||
ProductionStandardId: newStandard.Id,
|
||||
Week: detailReq.Week,
|
||||
TargetHenDayProduction: detailReq.ProductionStandardDetails.TargetHenDayProduction,
|
||||
TargetHenHouseProduction: detailReq.ProductionStandardDetails.TargetHenHouseProduction,
|
||||
TargetEggWeight: detailReq.ProductionStandardDetails.TargetEggWeight,
|
||||
TargetEggMass: detailReq.ProductionStandardDetails.TargetEggMass,
|
||||
}
|
||||
|
||||
if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil {
|
||||
return fmt.Errorf("failed to create production standard detail for week %d: %w", detailReq.Week, err)
|
||||
}
|
||||
}
|
||||
|
||||
standardGrowthDetail := &entity.StandardGrowthDetail{
|
||||
ProductionStandardId: newStandard.Id,
|
||||
Week: detailReq.Week,
|
||||
TargetMeanBw: detailReq.ProductionStandardUniformityDetails.TargetMeanBw,
|
||||
MaxDepletion: detailReq.ProductionStandardUniformityDetails.MaxDepletion,
|
||||
MinUniformity: detailReq.ProductionStandardUniformityDetails.MinUniformity,
|
||||
FeedIntake: detailReq.ProductionStandardUniformityDetails.FeedIntake,
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
|
||||
if err := standardGrowthDetailRepoTx.CreateOne(c.Context(), standardGrowthDetail, nil); err != nil {
|
||||
return fmt.Errorf("failed to create standard growth detail for week %d: %w", detailReq.Week, err)
|
||||
}
|
||||
}
|
||||
|
||||
createdStandard = newStandard
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to create production standard: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, createdStandard.Id)
|
||||
}
|
||||
|
||||
func (s productionStandardService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductionStandard, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var updatedStandard *entity.ProductionStandard
|
||||
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
||||
standardRepoTx := repository.NewProductionStandardRepository(tx)
|
||||
productionStandardDetailRepoTx := repository.NewProductionStandardDetailRepository(tx)
|
||||
standardGrowthDetailRepoTx := repository.NewStandardGrowthDetailRepository(tx)
|
||||
|
||||
existingStandard, err := standardRepoTx.GetByID(c.Context(), id, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "ProductionStandard not found")
|
||||
}
|
||||
return fmt.Errorf("failed to get production standard: %w", err)
|
||||
}
|
||||
|
||||
updateBody := make(map[string]any)
|
||||
if req.Name != nil {
|
||||
|
||||
nameExists, err := s.Repository.NameExists(c.Context(), *req.Name, &id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to check existing production standard: %+v", err)
|
||||
return err
|
||||
}
|
||||
if nameExists {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Production standard with name '%s' already exists", *req.Name))
|
||||
}
|
||||
updateBody["name"] = *req.Name
|
||||
}
|
||||
if req.ProjectCategory != nil {
|
||||
updateBody["project_category"] = *req.ProjectCategory
|
||||
}
|
||||
|
||||
if len(updateBody) > 0 {
|
||||
if err := standardRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||
return fmt.Errorf("failed to update production standard: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if req.Details != nil && len(req.Details) > 0 {
|
||||
|
||||
projectCategory := existingStandard.ProjectCategory
|
||||
if req.ProjectCategory != nil {
|
||||
projectCategory = *req.ProjectCategory
|
||||
}
|
||||
|
||||
if err := productionStandardDetailRepoTx.DeleteByProductionStandardID(c.Context(), id); err != nil {
|
||||
return fmt.Errorf("failed to delete old production standard details: %w", err)
|
||||
}
|
||||
if err := standardGrowthDetailRepoTx.DeleteByProductionStandardID(c.Context(), id); err != nil {
|
||||
return fmt.Errorf("failed to delete old standard growth details: %w", err)
|
||||
}
|
||||
|
||||
for _, detailReq := range req.Details {
|
||||
if detailReq.ProductionStandardUniformityDetails == nil {
|
||||
return fmt.Errorf("production_standard_uniformity_details is required in week %d", detailReq.Week)
|
||||
}
|
||||
|
||||
if projectCategory == "LAYING" {
|
||||
if detailReq.ProductionStandardDetails == nil {
|
||||
return fmt.Errorf("production_standard_details is required for LAYING category in week %d", detailReq.Week)
|
||||
}
|
||||
|
||||
productionStandardDetail := &entity.ProductionStandardDetail{
|
||||
ProductionStandardId: id,
|
||||
Week: detailReq.Week,
|
||||
TargetHenDayProduction: detailReq.ProductionStandardDetails.TargetHenDayProduction,
|
||||
TargetHenHouseProduction: detailReq.ProductionStandardDetails.TargetHenHouseProduction,
|
||||
TargetEggWeight: detailReq.ProductionStandardDetails.TargetEggWeight,
|
||||
TargetEggMass: detailReq.ProductionStandardDetails.TargetEggMass,
|
||||
}
|
||||
|
||||
if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil {
|
||||
return fmt.Errorf("failed to create production standard detail for week %d: %w", detailReq.Week, err)
|
||||
}
|
||||
}
|
||||
|
||||
standardGrowthDetail := &entity.StandardGrowthDetail{
|
||||
ProductionStandardId: id,
|
||||
Week: detailReq.Week,
|
||||
TargetMeanBw: detailReq.ProductionStandardUniformityDetails.TargetMeanBw,
|
||||
MaxDepletion: detailReq.ProductionStandardUniformityDetails.MaxDepletion,
|
||||
MinUniformity: detailReq.ProductionStandardUniformityDetails.MinUniformity,
|
||||
FeedIntake: detailReq.ProductionStandardUniformityDetails.FeedIntake,
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
|
||||
if err := standardGrowthDetailRepoTx.CreateOne(c.Context(), standardGrowthDetail, nil); err != nil {
|
||||
return fmt.Errorf("failed to create standard growth detail for week %d: %w", detailReq.Week, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updatedStandard = existingStandard
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to update production standard: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetOne(c, updatedStandard.Id)
|
||||
}
|
||||
|
||||
func (s productionStandardService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "ProductionStandard not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to delete productionStandard: %+v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package validation
|
||||
|
||||
type ProductionStandardDetailItem struct {
|
||||
TargetHenDayProduction *float64 `json:"target_hen_day_production" validate:"omitempty,gte=0"`
|
||||
TargetHenHouseProduction *float64 `json:"target_hen_house_production" validate:"omitempty,gte=0"`
|
||||
TargetEggWeight *float64 `json:"target_egg_weight" validate:"omitempty,gte=0"`
|
||||
TargetEggMass *float64 `json:"target_egg_mass" validate:"omitempty,gte=0"`
|
||||
}
|
||||
|
||||
type StandardGrowthDetailItem struct {
|
||||
TargetMeanBw *float64 `json:"target_mean_bw" validate:"omitempty,gte=0"`
|
||||
MaxDepletion *float64 `json:"max_depletion" validate:"omitempty,gte=0,lte=100"`
|
||||
MinUniformity float64 `json:"min_uniformity" validate:"required,gte=0,lte=100"`
|
||||
FeedIntake *float64 `json:"feed_intake" validate:"omitempty,gte=0"`
|
||||
}
|
||||
|
||||
type DetailItem struct {
|
||||
Week int `json:"week" validate:"required,gte=1"`
|
||||
ProductionStandardDetails *ProductionStandardDetailItem `json:"production_standard_details,omitempty"`
|
||||
ProductionStandardUniformityDetails *StandardGrowthDetailItem `json:"production_standard_uniformity_details" validate:"required"`
|
||||
}
|
||||
|
||||
|
||||
type Create struct {
|
||||
Name string `json:"name" validate:"required,min=3"`
|
||||
ProjectCategory string `json:"project_category" validate:"required,oneof=GROWING LAYING"`
|
||||
Details []DetailItem `json:"details" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,min=3"`
|
||||
ProjectCategory *string `json:"project_category,omitempty" validate:"omitempty,oneof=GROWING LAYING"`
|
||||
Details []DetailItem `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
ProjectCategory string `query:"project_category" validate:"omitempty,oneof=GROWING LAYING"`
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
suppliers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers"
|
||||
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
|
||||
warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses"
|
||||
productionStandards "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards"
|
||||
// MODULE IMPORTS
|
||||
)
|
||||
|
||||
@@ -40,6 +41,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
|
||||
products.ProductModule{},
|
||||
banks.BankModule{},
|
||||
flocks.FlockModule{},
|
||||
productionStandards.ProductionStandardModule{},
|
||||
// MODULE REGISTRY
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package chickins
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||
@@ -36,16 +38,43 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
||||
projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
|
||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
|
||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
||||
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
if err := fifoService.RegisterUsable(fifo.UsableConfig{
|
||||
Key: fifo.UsableKeyProjectChickin,
|
||||
Table: "project_chickins",
|
||||
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 chickin usable workflow: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register chickin approval workflow: %v", err))
|
||||
}
|
||||
|
||||
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
|
||||
chickinService := sChickin.NewChickinService(
|
||||
chickinRepo,
|
||||
kandangRepo,
|
||||
warehouseRepo,
|
||||
productWarehouseRepo,
|
||||
projectFlockRepo,
|
||||
projectflockkandangrepo,
|
||||
projectflockpopulationrepo,
|
||||
chickinDetailRepo,
|
||||
validate,
|
||||
fifoService)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
ChickinRoutes(router, userService, chickinService)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -15,7 +16,9 @@ import (
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations"
|
||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
@@ -23,6 +26,8 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var chickinUsableKey = fifo.UsableKeyProjectChickin
|
||||
|
||||
type ChickinService interface {
|
||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
|
||||
@@ -43,9 +48,11 @@ type chickinService struct {
|
||||
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
|
||||
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
|
||||
ProjectChickinDetailRepo repository.ProjectChickinDetailRepository
|
||||
FifoSvc commonSvc.FifoService
|
||||
StockLogRepo rStockLogs.StockLogRepository
|
||||
}
|
||||
|
||||
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate) ChickinService {
|
||||
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoSvc commonSvc.FifoService) ChickinService {
|
||||
return &chickinService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
@@ -57,6 +64,8 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan
|
||||
ProjectflockKandangRepo: projectflockkandangRepo,
|
||||
ProjectflockPopulationRepo: projectflockpopulationRepo,
|
||||
ProjectChickinDetailRepo: projectChickinDetailRepo,
|
||||
FifoSvc: fifoSvc,
|
||||
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,15 +133,14 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found")
|
||||
}
|
||||
|
||||
category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newChikins := make([]*entity.ProjectChickin, 0)
|
||||
chickinQtyMap := make(map[uint]float64)
|
||||
|
||||
for _, chickinReq := range req.ChickinRequests {
|
||||
for idx, chickinReq := range req.ChickinRequests {
|
||||
|
||||
productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickinReq.ProductWarehouseId, nil)
|
||||
if err != nil {
|
||||
@@ -152,26 +160,23 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId))
|
||||
}
|
||||
|
||||
availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, productWarehouse, category)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to calculate available quantity for product warehouse %d", chickinReq.ProductWarehouseId))
|
||||
}
|
||||
|
||||
availableQty := productWarehouse.Quantity
|
||||
if availableQty <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No available quantity for product warehouse %d", chickinReq.ProductWarehouseId))
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No available stock in product warehouse %d for chickin", chickinReq.ProductWarehouseId))
|
||||
}
|
||||
|
||||
newChickin := &entity.ProjectChickin{
|
||||
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
||||
ChickInDate: chickinDate,
|
||||
UsageQty: 0,
|
||||
PendingUsageQty: availableQty,
|
||||
PendingUsageQty: 0,
|
||||
ProductWarehouseId: chickinReq.ProductWarehouseId,
|
||||
Notes: chickinReq.Note,
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
|
||||
newChikins = append(newChikins, newChickin)
|
||||
chickinQtyMap[uint(idx)] = availableQty
|
||||
}
|
||||
|
||||
if len(newChikins) == 0 {
|
||||
@@ -188,30 +193,23 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
|
||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||
|
||||
if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
|
||||
}
|
||||
|
||||
for idx, chickin := range newChikins {
|
||||
desiredQty := chickinQtyMap[uint(idx)]
|
||||
if err := s.ConsumeChickinStocks(c.Context(), dbTransaction, chickin, desiredQty, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, projectFlockKandang.Id, nil)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval")
|
||||
}
|
||||
|
||||
if category == string(utils.ProjectFlockCategoryLaying) {
|
||||
for _, chickin := range newChikins {
|
||||
updates := map[string]any{"qty": gorm.Expr("qty - ?", chickin.PendingUsageQty)}
|
||||
|
||||
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickin.ProductWarehouseId))
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update product warehouse quantity")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var approvalAction entity.ApprovalAction
|
||||
if isFirstTime {
|
||||
approvalAction = entity.ApprovalActionCreated
|
||||
@@ -301,6 +299,32 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
|
||||
func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
|
||||
chickin, err := s.Repository.GetByID(c.Context(), id, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if chickin.UsageQty > 0 {
|
||||
if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
warehouseDeltas := make(map[uint]float64)
|
||||
warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty
|
||||
if err := s.adjustProductWarehouseQuantities(c.Context(), s.Repository.DB(), warehouseDeltas); err != nil {
|
||||
s.Log.Errorf("Failed to adjust product warehouses for deleted chickin %d: %+v", chickin.Id, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
||||
@@ -311,54 +335,6 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s chickinService) calculateAvailableQuantity(ctx *fiber.Ctx, projectFlockKandangID uint, productWarehouse *entity.ProductWarehouse, category string) (float64, error) {
|
||||
availableQty := productWarehouse.Quantity
|
||||
|
||||
if category == string(utils.ProjectFlockCategoryGrowing) {
|
||||
var totalPendingQty float64
|
||||
|
||||
chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
|
||||
if err == nil {
|
||||
for _, chickin := range chickins {
|
||||
|
||||
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
|
||||
totalPendingQty += chickin.PendingUsageQty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
availableQty = productWarehouse.Quantity - totalPendingQty
|
||||
if availableQty < 0 {
|
||||
availableQty = 0
|
||||
}
|
||||
} else if category == string(utils.ProjectFlockCategoryLaying) {
|
||||
var totalPopulation float64
|
||||
var totalPendingQty float64
|
||||
|
||||
populations, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(ctx.Context(), projectFlockKandangID, productWarehouse.Id)
|
||||
if err == nil {
|
||||
for _, pop := range populations {
|
||||
totalPopulation += pop.TotalQty
|
||||
}
|
||||
}
|
||||
chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
|
||||
if err == nil {
|
||||
for _, chickin := range chickins {
|
||||
|
||||
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
|
||||
totalPendingQty += chickin.PendingUsageQty
|
||||
}
|
||||
}
|
||||
}
|
||||
availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty
|
||||
if availableQty < 0 {
|
||||
availableQty = 0
|
||||
}
|
||||
}
|
||||
|
||||
return availableQty, nil
|
||||
}
|
||||
|
||||
func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
@@ -387,11 +363,10 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
||||
}
|
||||
|
||||
for _, id := range approvableIDs {
|
||||
idCopy := id
|
||||
if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &idCopy, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil {
|
||||
|
||||
if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &id, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, id, nil)
|
||||
|
||||
if err != nil {
|
||||
@@ -414,7 +389,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
||||
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
||||
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||
|
||||
for _, approvableID := range approvableIDs {
|
||||
if _, err := approvalSvc.CreateApproval(
|
||||
@@ -472,9 +446,9 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
||||
}
|
||||
|
||||
pfkID := approvableID
|
||||
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID, &pfkID)
|
||||
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "LAYER", dbTransaction, actorID, &pfkID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create LAYER product warehouse")
|
||||
}
|
||||
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
|
||||
@@ -491,27 +465,17 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
||||
continue
|
||||
}
|
||||
|
||||
kandangForRejection, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID))
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang")
|
||||
}
|
||||
|
||||
categoryForRejection := strings.ToUpper(strings.TrimSpace(kandangForRejection.ProjectFlock.Category))
|
||||
|
||||
for _, chickin := range chickins {
|
||||
|
||||
if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) {
|
||||
updates := map[string]any{"qty": gorm.Expr("qty + ?", chickin.PendingUsageQty)}
|
||||
if err := s.ReleaseChickinStocks(c.Context(), dbTransaction, &chickin, actorID); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for rejected chickin %d: %v", chickin.Id, err))
|
||||
}
|
||||
|
||||
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId))
|
||||
}
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to restore product warehouse quantity for chickin %d", chickin.Id))
|
||||
}
|
||||
warehouseDeltas := make(map[uint]float64)
|
||||
warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty
|
||||
if err := s.adjustProductWarehouseQuantities(c.Context(), dbTransaction, warehouseDeltas); err != nil {
|
||||
s.Log.Errorf("Failed to adjust product warehouses for rejected chickin %d: %+v", chickin.Id, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil {
|
||||
@@ -549,7 +513,7 @@ func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId
|
||||
products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
|
||||
if err == nil && len(products) > 0 {
|
||||
existingPW := &products[0]
|
||||
// Update project_flock_kandang_id if not already set
|
||||
|
||||
if existingPW.ProjectFlockKandangId == nil && projectFlockKandangId != nil {
|
||||
existingPW.ProjectFlockKandangId = projectFlockKandangId
|
||||
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).UpdateOne(ctx.Context(), existingPW.Id, existingPW, nil); err != nil {
|
||||
@@ -572,7 +536,6 @@ func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId
|
||||
WarehouseId: warehouseId,
|
||||
ProjectFlockKandangId: projectFlockKandangId,
|
||||
Quantity: 0,
|
||||
// CreatedBy: actorID,
|
||||
}
|
||||
|
||||
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil {
|
||||
@@ -588,10 +551,10 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
||||
return fmt.Errorf("invalid target product warehouse")
|
||||
}
|
||||
|
||||
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
||||
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
|
||||
|
||||
var totalQuantityAdded float64
|
||||
|
||||
for _, chickin := range chickins {
|
||||
|
||||
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id)
|
||||
@@ -604,34 +567,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
||||
continue
|
||||
}
|
||||
|
||||
quantityToConvert := chickin.PendingUsageQty
|
||||
|
||||
if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{
|
||||
"usage_qty": quantityToConvert,
|
||||
"pending_usage_qty": 0,
|
||||
}, nil); err != nil {
|
||||
return fmt.Errorf("failed to update chickin %d qty: %w", chickin.Id, err)
|
||||
}
|
||||
|
||||
if chickin.ProductWarehouseId != targetPW.Id {
|
||||
if err := productWarehouseTx.PatchOne(ctx.Context(), chickin.ProductWarehouseId, map[string]any{
|
||||
"qty": gorm.Expr("qty - ?", quantityToConvert),
|
||||
}, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Source product warehouse %d not found", chickin.ProductWarehouseId))
|
||||
}
|
||||
return fmt.Errorf("failed to deduct source warehouse quantity for chickin %d: %w", chickin.Id, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{
|
||||
"qty": gorm.Expr("qty + ?", quantityToConvert),
|
||||
}, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id))
|
||||
}
|
||||
return fmt.Errorf("failed to update target warehouse quantity: %w", err)
|
||||
}
|
||||
quantityToConvert := chickin.UsageQty
|
||||
|
||||
population := &entity.ProjectFlockPopulation{
|
||||
ProjectChickinId: chickin.Id,
|
||||
@@ -644,7 +580,121 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
||||
if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totalQuantityAdded += quantityToConvert
|
||||
}
|
||||
|
||||
if totalQuantityAdded > 0 {
|
||||
if err := s.ProductWarehouseRepo.AdjustQuantities(ctx.Context(), map[uint]float64{
|
||||
targetPW.Id: totalQuantityAdded,
|
||||
}, func(db *gorm.DB) *gorm.DB {
|
||||
return dbTransaction
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to update target product warehouse quantity: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, desiredQty float64, actorID uint) error {
|
||||
if chickin == nil || s.FifoSvc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.Log.Infof("ConsumeChickinStocks: chickin_id=%d, product_warehouse_id=%d, desired_qty=%.3f",
|
||||
chickin.Id, chickin.ProductWarehouseId, desiredQty)
|
||||
|
||||
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
||||
UsableKey: chickinUsableKey,
|
||||
UsableID: chickin.Id,
|
||||
ProductWarehouseID: chickin.ProductWarehouseId,
|
||||
Quantity: desiredQty,
|
||||
AllowPending: true,
|
||||
Tx: tx,
|
||||
})
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to consume FIFO stock for chickin %d: %+v", chickin.Id, err)
|
||||
return err
|
||||
}
|
||||
|
||||
s.Log.Infof("ConsumeChickinStocks result: usage_qty=%.3f, pending_qty=%.3f, allocated_allocations=%d",
|
||||
result.UsageQuantity, result.PendingQuantity, len(result.AddedAllocations))
|
||||
|
||||
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Updates(map[string]interface{}{
|
||||
"usage_qty": result.UsageQuantity,
|
||||
"pending_usage_qty": result.PendingQuantity,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.UsageQuantity > 0 {
|
||||
decreaseLog := &entity.StockLog{
|
||||
Decrease: result.UsageQuantity,
|
||||
LoggableType: string(utils.StockLogTypeChikin),
|
||||
LoggableId: chickin.Id,
|
||||
ProductWarehouseId: chickin.ProductWarehouseId,
|
||||
CreatedBy: actorID,
|
||||
Notes: fmt.Sprintf("Chickin #%d", chickin.Id),
|
||||
}
|
||||
if err := s.StockLogRepo.CreateOne(ctx, decreaseLog, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock log for chickin %d: %+v", chickin.Id, err)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, actorID uint) error {
|
||||
if chickin == nil || s.FifoSvc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var currentUsage float64
|
||||
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(¤tUsage).Error; err != nil {
|
||||
s.Log.Warnf("Failed to get current usage for chickin %d: %+v", chickin.Id, err)
|
||||
currentUsage = 0
|
||||
}
|
||||
|
||||
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
|
||||
UsableKey: chickinUsableKey,
|
||||
UsableID: chickin.Id,
|
||||
Tx: tx,
|
||||
}); err != nil {
|
||||
s.Log.Errorf("Failed to release FIFO stock for chickin %d: %+v", chickin.Id, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Updates(map[string]interface{}{
|
||||
"usage_qty": 0,
|
||||
"pending_usage_qty": 0,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create stock log for the restoration
|
||||
if currentUsage > 0 {
|
||||
increaseLog := &entity.StockLog{
|
||||
Increase: currentUsage,
|
||||
LoggableType: string(utils.StockLogTypeChikin),
|
||||
LoggableId: chickin.Id,
|
||||
ProductWarehouseId: chickin.ProductWarehouseId,
|
||||
CreatedBy: actorID,
|
||||
Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id),
|
||||
}
|
||||
if err := s.StockLogRepo.CreateOne(ctx, increaseLog, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create stock log for chickin %d: %+v", chickin.Id, err)
|
||||
// Don't return error here, stock already released
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *chickinService) adjustProductWarehouseQuantities(ctx context.Context, tx *gorm.DB, deltas map[uint]float64) error {
|
||||
if len(deltas) == 0 {
|
||||
return nil
|
||||
}
|
||||
return s.ProductWarehouseRepo.AdjustQuantities(ctx, deltas, func(*gorm.DB) *gorm.DB { return tx })
|
||||
}
|
||||
|
||||
+3
-9
@@ -555,6 +555,7 @@ func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehous
|
||||
availableQty := productWarehouse.Quantity
|
||||
|
||||
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
|
||||
|
||||
var totalPendingQty float64
|
||||
|
||||
for _, chickin := range projectFlockKandang.Chickins {
|
||||
@@ -568,15 +569,8 @@ func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehous
|
||||
availableQty = 0
|
||||
}
|
||||
} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
||||
var totalPopulation float64
|
||||
var totalPendingQty float64
|
||||
|
||||
populations, err := s.PopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(c.Context(), projectFlockKandang.Id, productWarehouse.Id)
|
||||
if err == nil {
|
||||
for _, pop := range populations {
|
||||
totalPopulation += pop.TotalQty
|
||||
}
|
||||
}
|
||||
var totalPendingQty float64
|
||||
|
||||
for _, chickin := range projectFlockKandang.Chickins {
|
||||
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
|
||||
@@ -584,7 +578,7 @@ func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehous
|
||||
}
|
||||
}
|
||||
|
||||
availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty
|
||||
availableQty = productWarehouse.Quantity - totalPendingQty
|
||||
if availableQty < 0 {
|
||||
availableQty = 0
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package purchases
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
@@ -41,6 +42,11 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
documentRepo := commonRepo.NewDocumentRepository(db)
|
||||
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to create document service: %v", err))
|
||||
}
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPurchase, utils.PurchaseApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register purchase approval workflow: %v", err))
|
||||
}
|
||||
@@ -54,6 +60,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
||||
approvalService,
|
||||
expenseRealizationRepo,
|
||||
projectFlockKandangRepository,
|
||||
documentSvc,
|
||||
validate,
|
||||
)
|
||||
expenseBridge := service.NewExpenseBridge(
|
||||
|
||||
@@ -61,7 +61,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
|
||||
|
||||
doNumber := marketingDTO.GenerateDeliveryOrderNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId)
|
||||
|
||||
totalWeightKg := mdp.Qty * mdp.AvgWeight
|
||||
totalWeightKg := mdp.UsageQty * mdp.AvgWeight
|
||||
salesAmount := totalWeightKg * mdp.UnitPrice
|
||||
|
||||
var hpp float64
|
||||
@@ -78,7 +78,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
|
||||
AgingDays: agingDays,
|
||||
DoNumber: doNumber,
|
||||
MarketingType: getMarketingType(mdp),
|
||||
Qty: mdp.Qty,
|
||||
Qty: mdp.UsageQty,
|
||||
AverageWeightKg: mdp.AvgWeight,
|
||||
TotalWeightKg: totalWeightKg,
|
||||
SalesPricePerKg: mdp.UnitPrice,
|
||||
@@ -194,8 +194,8 @@ func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, ca
|
||||
totalHppAmount := int64(0)
|
||||
|
||||
for _, mdp := range mdps {
|
||||
calculatedTotalWeight := mdp.Qty * mdp.AvgWeight
|
||||
totalQty += int(mdp.Qty)
|
||||
calculatedTotalWeight := mdp.UsageQty * mdp.AvgWeight
|
||||
totalQty += int(mdp.UsageQty)
|
||||
totalWeightKg += calculatedTotalWeight
|
||||
totalSalesAmount += int64(calculatedTotalWeight * mdp.UnitPrice)
|
||||
|
||||
|
||||
@@ -138,7 +138,6 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing
|
||||
func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, category string) float64 {
|
||||
totalCost := s.getTotalProjectCost(ctx, projectFlockID)
|
||||
if totalCost == 0 {
|
||||
s.Log.Warnf("HPP calculation: No cost found for project flock ID %d. Check if purchase items are linked to project_flock_kandang_id", projectFlockID)
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ import (
|
||||
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
|
||||
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
|
||||
purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases"
|
||||
repports "gitlab.com/mbugroup/lti-api.git/internal/modules/repports"
|
||||
ssoModule "gitlab.com/mbugroup/lti-api.git/internal/modules/sso"
|
||||
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
|
||||
repports "gitlab.com/mbugroup/lti-api.git/internal/modules/repports"
|
||||
// MODULE IMPORTS
|
||||
)
|
||||
|
||||
@@ -43,7 +43,8 @@ func Routes(app *fiber.App, db *gorm.DB) {
|
||||
expenses.ExpenseModule{},
|
||||
ssoModule.Module{},
|
||||
closings.ClosingModule{},
|
||||
repports.RepportModule{},
|
||||
repports.RepportModule{},
|
||||
|
||||
// MODULE REGISTRY
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,8 @@ type StockLogType string
|
||||
const (
|
||||
StockLogTypeAdjustment StockLogType = "ADJUSTMENT"
|
||||
StockLogTypeTransfer StockLogType = "TRANSFER"
|
||||
StockLogTypeMarketing StockLogType = "MARKETING"
|
||||
StockLogTypeChikin StockLogType = "CHICKIN"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
@@ -314,6 +316,23 @@ var ExpenseApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||
ExpenseStepSelesai: "Selesai",
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Document
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type DocumentType string
|
||||
type DocumentableType string
|
||||
|
||||
const (
|
||||
DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT"
|
||||
DocumentTypeExpense DocumentType = "EXPENSE_DOCUMENT"
|
||||
DocumentTypeExpenseRealization DocumentType = "EXPENSE_REALIZATION_DOCUMENT"
|
||||
|
||||
DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER"
|
||||
DocumentableTypeExpense DocumentableType = "EXPENSE"
|
||||
DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION"
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Validators
|
||||
// -------------------------------------------------------------------
|
||||
@@ -448,7 +467,7 @@ func IsValidExpenseCategory(v string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// example use
|
||||
// e xample use
|
||||
|
||||
// Recording helper
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package fifo
|
||||
|
||||
const (
|
||||
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
||||
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
||||
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
|
||||
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user