mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 07:15:43 +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
|
var lots []stockLot
|
||||||
for key, cfg := range configs {
|
for key, cfg := range configs {
|
||||||
selectStmt := fmt.Sprintf(
|
|
||||||
"%s AS id, %s AS available_qty, %s AS created_at",
|
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
|
||||||
cfg.Columns.ID,
|
|
||||||
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
|
var selectStmt string
|
||||||
cfg.Columns.CreatedAt,
|
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 {
|
var rows []struct {
|
||||||
ID uint
|
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
|
package entities
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -13,8 +12,6 @@ type Expense struct {
|
|||||||
SupplierId uint64 `gorm:""`
|
SupplierId uint64 `gorm:""`
|
||||||
Category string `gorm:"type:varchar(50);not null"`
|
Category string `gorm:"type:varchar(50);not null"`
|
||||||
PoNumber string `gorm:"type:varchar(50)"`
|
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"`
|
RealizationDate time.Time `gorm:"type:date;column:realization_date"`
|
||||||
TransactionDate time.Time `gorm:"type:date;not null"`
|
TransactionDate time.Time `gorm:"type:date;not null"`
|
||||||
Notes string `gorm:"type:text;column:notes"`
|
Notes string `gorm:"type:text;column:notes"`
|
||||||
@@ -23,8 +20,10 @@ type Expense struct {
|
|||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
|
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
|
||||||
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
|
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 {
|
type MarketingDeliveryProduct struct {
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
Id uint `gorm:"primaryKey;autoIncrement"`
|
||||||
MarketingProductId uint `gorm:"uniqueIndex;not null"`
|
MarketingProductId uint `gorm:"uniqueIndex;not null"`
|
||||||
Qty float64 `gorm:"type:numeric(15,3)"`
|
ProductWarehouseId uint `gorm:"not null"`
|
||||||
UnitPrice float64 `gorm:"type:numeric(15,3)"`
|
UnitPrice float64 `gorm:"type:numeric(15,3)"`
|
||||||
TotalWeight float64 `gorm:"type:numeric(15,3)"`
|
TotalWeight float64 `gorm:"type:numeric(15,3)"`
|
||||||
AvgWeight float64 `gorm:"type:numeric(15,3)"`
|
AvgWeight float64 `gorm:"type:numeric(15,3)"`
|
||||||
TotalPrice float64 `gorm:"type:numeric(15,3)"`
|
TotalPrice float64 `gorm:"type:numeric(15,3)"`
|
||||||
DeliveryDate *time.Time `gorm:"type:timestamptz"`
|
DeliveryDate *time.Time `gorm:"type:timestamptz"`
|
||||||
VehicleNumber string `gorm:"type:varchar(50)"`
|
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"`
|
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"`
|
Details []StockTransferDetail `gorm:"foreignKey:StockTransferId"`
|
||||||
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
|
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy"`
|
||||||
|
Documents []Document `gorm:"foreignKey:DocumentableId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,6 @@ package entities
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
const (
|
|
||||||
LogTypeAdjustment = "ADJUSTMENT"
|
|
||||||
LogTypeTransfer = "TRANSFER"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
TransactionTypeIncrease = "INCREASE"
|
|
||||||
TransactionTypeDecrease = "DECREASE"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StockLog struct {
|
type StockLog struct {
|
||||||
Id uint `gorm:"primaryKey;column:id"`
|
Id uint `gorm:"primaryKey;column:id"`
|
||||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null;index"`
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null;index"`
|
||||||
|
|||||||
@@ -4,20 +4,21 @@ import "time"
|
|||||||
|
|
||||||
// DETAIL EKSPEDISI
|
// DETAIL EKSPEDISI
|
||||||
type StockTransferDelivery struct {
|
type StockTransferDelivery struct {
|
||||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||||
StockTransferId uint64
|
StockTransferId uint64
|
||||||
SupplierId uint64
|
SupplierId uint64
|
||||||
VehiclePlate string
|
VehiclePlate string
|
||||||
DriverName string
|
DriverName string
|
||||||
DocumentNumber string
|
DocumentNumber string
|
||||||
DocumentPath string
|
DocumentPath string
|
||||||
ShippingCostItem float64
|
ShippingCostItem float64
|
||||||
ShippingCostTotal float64
|
ShippingCostTotal float64
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
DeletedAt *time.Time `gorm:"index"`
|
DeletedAt *time.Time `gorm:"index"`
|
||||||
// Relations
|
// Relations
|
||||||
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
|
||||||
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
|
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
|
||||||
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
|
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_DeliveryGetAll = "lti.marketing.delivery_order.list"
|
||||||
P_DeliveryGetOne = "lti.marketing.delivery_order.detail"
|
P_DeliveryGetOne = "lti.marketing.delivery_order.detail"
|
||||||
P_DeliveryUpdateOne = "lti.marketing.delivery_order.update"
|
P_DeliveryUpdateOne = "lti.marketing.delivery_order.update"
|
||||||
|
P_DeliveryCreateOne = "lti.marketing.delivery_order.Create"
|
||||||
P_SalesOrderDelete = "lti.marketing.sales_order.delete"
|
P_SalesOrderDelete = "lti.marketing.sales_order.delete"
|
||||||
P_SalesOrderApproval = "lti.marketing.sales_order.approve"
|
P_SalesOrderApproval = "lti.marketing.sales_order.approve"
|
||||||
P_SalesOrderCreateOne = "lti.marketing.sales_order.create"
|
P_SalesOrderCreateOne = "lti.marketing.sales_order.create"
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
|
|||||||
DoNumber: doNumber,
|
DoNumber: doNumber,
|
||||||
Product: product,
|
Product: product,
|
||||||
Customer: customer,
|
Customer: customer,
|
||||||
Qty: e.Qty,
|
Qty: e.UsageQty, // Show allocated quantity from FIFO
|
||||||
Weight: e.TotalWeight,
|
Weight: e.TotalWeight,
|
||||||
AvgWeight: e.AvgWeight,
|
AvgWeight: e.AvgWeight,
|
||||||
Price: e.UnitPrice,
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, nil, err
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -712,13 +712,13 @@ func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlo
|
|||||||
)
|
)
|
||||||
|
|
||||||
for _, product := range deliveryProducts {
|
for _, product := range deliveryProducts {
|
||||||
if product.Qty == 0 {
|
if product.UsageQty == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
projectFlockKandang := product.MarketingProduct.ProductWarehouse.ProjectFlockKandang
|
projectFlockKandang := product.MarketingProduct.ProductWarehouse.ProjectFlockKandang
|
||||||
ageWeeks := dto.CalculateAgeFromChickinDataProduksi(projectFlockKandang, product.DeliveryDate)
|
ageWeeks := dto.CalculateAgeFromChickinDataProduksi(projectFlockKandang, product.DeliveryDate)
|
||||||
totalAgeWeeks += float64(ageWeeks) * product.Qty
|
totalAgeWeeks += float64(ageWeeks) * product.UsageQty
|
||||||
totalQty += product.Qty
|
totalQty += product.UsageQty
|
||||||
}
|
}
|
||||||
|
|
||||||
if totalQty == 0 {
|
if totalQty == 0 {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
@@ -41,8 +40,8 @@ type ExpenseListDTO struct {
|
|||||||
|
|
||||||
type ExpenseDetailDTO struct {
|
type ExpenseDetailDTO struct {
|
||||||
ExpenseBaseDTO
|
ExpenseBaseDTO
|
||||||
Documents []DocumentDTO `json:"documents,omitempty"`
|
Documents []DocumentDTO `json:"documents"`
|
||||||
RealizationDocs []DocumentDTO `json:"realization_docs,omitempty"`
|
RealizationDocs []DocumentDTO `json:"realization_docs"`
|
||||||
Kandangs []KandangGroupDTO `json:"kandangs,omitempty"`
|
Kandangs []KandangGroupDTO `json:"kandangs,omitempty"`
|
||||||
TotalPengajuan float64 `json:"total_pengajuan"`
|
TotalPengajuan float64 `json:"total_pengajuan"`
|
||||||
TotalRealisasi float64 `json:"total_realisasi"`
|
TotalRealisasi float64 `json:"total_realisasi"`
|
||||||
@@ -179,12 +178,20 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
|
|||||||
var pengajuans []ExpenseNonstockDTO
|
var pengajuans []ExpenseNonstockDTO
|
||||||
var realisasi []ExpenseRealizationDTO
|
var realisasi []ExpenseRealizationDTO
|
||||||
|
|
||||||
if e.DocumentPath.Valid && e.DocumentPath.String != "" {
|
// Map documents from Document service
|
||||||
json.Unmarshal([]byte(e.DocumentPath.String), &documents)
|
for _, doc := range e.Documents {
|
||||||
|
documents = append(documents, DocumentDTO{
|
||||||
|
ID: uint64(doc.Id),
|
||||||
|
Path: doc.Path,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.RealizationDocumentPath.Valid && e.RealizationDocumentPath.String != "" {
|
// Map realization documents from Document service
|
||||||
json.Unmarshal([]byte(e.RealizationDocumentPath.String), &realizationDocs)
|
for _, doc := range e.RealizationDocuments {
|
||||||
|
realizationDocs = append(realizationDocs, DocumentDTO{
|
||||||
|
ID: uint64(doc.Id),
|
||||||
|
Path: doc.Path,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(e.Nonstocks) > 0 {
|
if len(e.Nonstocks) > 0 {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package expenses
|
package expenses
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
@@ -31,15 +32,20 @@ func (ExpenseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
realizationRepo := rExpense.NewExpenseRealizationRepository(db)
|
realizationRepo := rExpense.NewExpenseRealizationRepository(db)
|
||||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
|
documentRepo := commonRepo.NewDocumentRepository(db)
|
||||||
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
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
|
// Register workflow steps for EXPENSES approval
|
||||||
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
|
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
|
||||||
panic(fmt.Sprintf("failed to register expense approval workflow: %v", err))
|
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)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ExpenseRoutes(router, userService, expenseService)
|
ExpenseRoutes(router, userService, expenseService)
|
||||||
|
|||||||
@@ -2,11 +2,8 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime/multipart"
|
|
||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
@@ -49,9 +46,10 @@ type expenseService struct {
|
|||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
RealizationRepository repository.ExpenseRealizationRepository
|
RealizationRepository repository.ExpenseRealizationRepository
|
||||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
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{
|
return &expenseService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
@@ -61,6 +59,7 @@ func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierR
|
|||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
RealizationRepository: realizationRepo,
|
RealizationRepository: realizationRepo,
|
||||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
|
DocumentSvc: documentSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +71,13 @@ func (s expenseService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("Nonstocks.Realization").
|
Preload("Nonstocks.Realization").
|
||||||
Preload("Nonstocks.ProjectFlockKandang.Kandang.Location").
|
Preload("Nonstocks.ProjectFlockKandang.Kandang.Location").
|
||||||
Preload("Nonstocks.Kandang").
|
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) {
|
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")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create initial approval")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.Documents) > 0 {
|
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
||||||
if err := s.processDocuments(c, expenseRepoTx, uint(expense.Id), req.Documents, false); err != nil {
|
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
|
||||||
return err
|
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 s.DocumentSvc != nil && len(req.Documents) > 0 {
|
||||||
if err := s.processDocuments(c, expenseRepoTx, id, req.Documents, false); err != nil {
|
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
|
||||||
return err
|
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")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.Documents) > 0 {
|
if s.DocumentSvc != nil && len(req.Documents) > 0 {
|
||||||
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
|
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
|
||||||
return err
|
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 s.DocumentSvc != nil && len(req.Documents) > 0 {
|
||||||
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
|
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
|
||||||
return err
|
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
|
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 {
|
func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error {
|
||||||
|
|
||||||
if err := commonSvc.EnsureRelations(ctx.Context(),
|
if err := commonSvc.EnsureRelations(ctx.Context(),
|
||||||
@@ -951,62 +941,40 @@ func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, document
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.DB().WithContext(ctx.Context()).Transaction(func(tx *gorm.DB) error {
|
if s.DocumentSvc == nil {
|
||||||
expenseRepoTx := repository.NewExpenseRepository(tx)
|
return fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
|
||||||
|
}
|
||||||
|
|
||||||
expense, err := expenseRepoTx.GetByID(ctx.Context(), expenseID, nil)
|
// Verify document exists and belongs to the expense
|
||||||
if err != nil {
|
var documentableType string
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense for document deletion")
|
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
|
if !documentFound {
|
||||||
var fieldName string
|
return fiber.NewError(fiber.StatusNotFound, "Document not found")
|
||||||
|
}
|
||||||
|
|
||||||
if isRealization {
|
// Delete document from database and storage
|
||||||
fieldName = "realization_document_path"
|
if err := s.DocumentSvc.DeleteDocuments(ctx.Context(), documentIDsToDelete, true); err != nil {
|
||||||
if expense.RealizationDocumentPath.Valid && expense.RealizationDocumentPath.String != "" {
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete document")
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, err
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if stockLog.LoggableType != entity.LogTypeAdjustment {
|
if stockLog.LoggableType != string(utils.StockLogTypeAdjustment) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
|
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")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Quantity must be greater than zero")
|
||||||
}
|
}
|
||||||
transactionType := strings.ToUpper(req.TransactionType)
|
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")
|
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
|
afterQuantity := productWarehouse.Quantity
|
||||||
newLog := &entity.StockLog{
|
newLog := &entity.StockLog{
|
||||||
// TransactionType: transactionType,
|
|
||||||
LoggableType: entity.LogTypeAdjustment,
|
LoggableType: string(utils.StockLogTypeAdjustment),
|
||||||
LoggableId: 0,
|
LoggableId: 0,
|
||||||
Notes: req.Note,
|
Notes: req.Note,
|
||||||
ProductWarehouseId: productWarehouse.Id,
|
ProductWarehouseId: productWarehouse.Id,
|
||||||
CreatedBy: actorID, // TODO: should Get from auth middleware
|
CreatedBy: actorID, // TODO: should Get from auth middleware
|
||||||
}
|
}
|
||||||
if transactionType == entity.TransactionTypeIncrease {
|
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
|
||||||
afterQuantity += req.Quantity
|
afterQuantity += req.Quantity
|
||||||
newLog.Increase = afterQuantity
|
newLog.Increase = afterQuantity
|
||||||
} else {
|
} else {
|
||||||
@@ -248,7 +248,7 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
|
|||||||
|
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
|
|
||||||
db = db.Where("loggable_type = ?", entity.LogTypeAdjustment)
|
db = db.Where("loggable_type = ?", string(utils.StockLogTypeAdjustment))
|
||||||
|
|
||||||
if query.TransactionType != "" {
|
if query.TransactionType != "" {
|
||||||
db = db.Where("transaction_type = ?", strings.ToUpper(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,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get transfer successfully",
|
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")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ambil file
|
|
||||||
form, err := c.MultipartForm()
|
form, err := c.MultipartForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -98,6 +102,6 @@ func (u *TransferController) CreateOne(c *fiber.Ctx) error {
|
|||||||
Code: fiber.StatusCreated,
|
Code: fiber.StatusCreated,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Create transfer successfully",
|
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"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// === DTO Structs ===
|
|
||||||
|
|
||||||
type TransferRelationDTO struct {
|
type TransferRelationDTO struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
TransferReason string `json:"transfer_reason"`
|
TransferReason string `json:"transfer_reason"`
|
||||||
@@ -17,7 +15,6 @@ type TransferRelationDTO struct {
|
|||||||
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
|
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only id and name for warehouse simple view
|
|
||||||
type WarehouseSimpleDTO struct {
|
type WarehouseSimpleDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -43,6 +40,14 @@ type SupplierSimpleDTO struct {
|
|||||||
Name string `json:"name"`
|
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 {
|
type WarehouseDetailDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -65,24 +70,22 @@ type TransferDetailDTO struct {
|
|||||||
Deliveries []TransferDeliveryDTO `json:"deliveries"`
|
Deliveries []TransferDeliveryDTO `json:"deliveries"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detail produk
|
|
||||||
type TransferDetailItemDTO struct {
|
type TransferDetailItemDTO struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
Proudct ProductSimpleDTO `json:"product"`
|
Product ProductSimpleDTO `json:"product"`
|
||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delivery ekspedisi
|
|
||||||
type TransferDeliveryDTO struct {
|
type TransferDeliveryDTO struct {
|
||||||
Id uint64 `json:"id"`
|
Id uint64 `json:"id"`
|
||||||
Supplier SupplierSimpleDTO `json:"supplier"`
|
Supplier SupplierSimpleDTO `json:"supplier"`
|
||||||
VehiclePlate string `json:"vehicle_plate"`
|
VehiclePlate string `json:"vehicle_plate"`
|
||||||
DriverName string `json:"driver_name"`
|
DriverName string `json:"driver_name"`
|
||||||
DocumentNumber string `json:"document_number"`
|
DocumentNumber string `json:"document_number"`
|
||||||
DocumentPath string `json:"document_path"`
|
|
||||||
ShippingCostItem float64 `json:"shipping_cost_item"`
|
ShippingCostItem float64 `json:"shipping_cost_item"`
|
||||||
ShippingCostTotal float64 `json:"shipping_cost_total"`
|
ShippingCostTotal float64 `json:"shipping_cost_total"`
|
||||||
Items []TransferDeliveryItemDTO `json:"items"`
|
Items []TransferDeliveryItemDTO `json:"items"`
|
||||||
|
Document *DocumentDTO `json:"document,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransferDeliveryItemDTO struct {
|
type TransferDeliveryItemDTO struct {
|
||||||
@@ -91,10 +94,7 @@ type TransferDeliveryItemDTO struct {
|
|||||||
Quantity float64 `json:"quantity"`
|
Quantity float64 `json:"quantity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Mapper Functions ===
|
|
||||||
|
|
||||||
func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
|
func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
|
||||||
|
|
||||||
var sourceWarehouse *WarehouseDetailDTO
|
var sourceWarehouse *WarehouseDetailDTO
|
||||||
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
|
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
|
||||||
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
|
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
|
||||||
@@ -140,7 +140,7 @@ func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
|
|||||||
Id: w.Id,
|
Id: w.Id,
|
||||||
Name: w.Name,
|
Name: w.Name,
|
||||||
Location: toLocationDTO(w.Location),
|
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)
|
mapped := userDTO.ToUserRelationDTO(*e.CreatedUser)
|
||||||
createdUser = &mapped
|
createdUser = &mapped
|
||||||
}
|
}
|
||||||
// Map details
|
|
||||||
var details []TransferDetailItemDTO
|
var details []TransferDetailItemDTO
|
||||||
for _, d := range e.Details {
|
for _, d := range e.Details {
|
||||||
details = append(details, TransferDetailItemDTO{
|
details = append(details, TransferDetailItemDTO{
|
||||||
Id: d.Id,
|
Id: d.Id,
|
||||||
Proudct: ProductSimpleDTO{
|
Product: ProductSimpleDTO{
|
||||||
Id: d.Product.Id,
|
Id: d.Product.Id,
|
||||||
Name: d.Product.Name,
|
Name: d.Product.Name,
|
||||||
},
|
},
|
||||||
Quantity: d.Quantity,
|
Quantity: d.Quantity,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Map deliveries
|
|
||||||
var deliveries []TransferDeliveryDTO
|
var deliveries []TransferDeliveryDTO
|
||||||
for _, del := range e.Deliveries {
|
for _, del := range e.Deliveries {
|
||||||
// Map delivery items
|
|
||||||
var items []TransferDeliveryItemDTO
|
var items []TransferDeliveryItemDTO
|
||||||
for _, item := range del.Items {
|
for _, item := range del.Items {
|
||||||
items = append(items, TransferDeliveryItemDTO{
|
items = append(items, TransferDeliveryItemDTO{
|
||||||
@@ -174,6 +173,19 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
|||||||
Quantity: item.Quantity,
|
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{
|
deliveries = append(deliveries, TransferDeliveryDTO{
|
||||||
Id: del.Id,
|
Id: del.Id,
|
||||||
Supplier: SupplierSimpleDTO{
|
Supplier: SupplierSimpleDTO{
|
||||||
@@ -183,12 +195,13 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
|
|||||||
VehiclePlate: del.VehiclePlate,
|
VehiclePlate: del.VehiclePlate,
|
||||||
DriverName: del.DriverName,
|
DriverName: del.DriverName,
|
||||||
DocumentNumber: del.DocumentNumber,
|
DocumentNumber: del.DocumentNumber,
|
||||||
DocumentPath: del.DocumentPath,
|
|
||||||
ShippingCostItem: del.ShippingCostItem,
|
ShippingCostItem: del.ShippingCostItem,
|
||||||
ShippingCostTotal: del.ShippingCostTotal,
|
ShippingCostTotal: del.ShippingCostTotal,
|
||||||
Items: items,
|
Items: items,
|
||||||
|
Document: document,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return TransferListDTO{
|
return TransferListDTO{
|
||||||
TransferRelationDTO: ToTransferRelationDTO(e),
|
TransferRelationDTO: ToTransferRelationDTO(e),
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
@@ -208,21 +221,32 @@ func ToTransferListDTOs(e []entity.StockTransfer) []TransferListDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
||||||
// Map details
|
|
||||||
var details []TransferDetailItemDTO
|
var details []TransferDetailItemDTO
|
||||||
for _, d := range e.Details {
|
for _, d := range e.Details {
|
||||||
details = append(details, TransferDetailItemDTO{
|
details = append(details, TransferDetailItemDTO{
|
||||||
Id: d.Id,
|
Id: d.Id,
|
||||||
Proudct: ProductSimpleDTO{
|
Product: ProductSimpleDTO{
|
||||||
Id: d.Product.Id,
|
Id: d.Product.Id,
|
||||||
Name: d.Product.Name,
|
Name: d.Product.Name,
|
||||||
},
|
},
|
||||||
Quantity: d.Quantity,
|
Quantity: d.Quantity,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Map deliveries
|
|
||||||
var deliveries []TransferDeliveryDTO
|
var deliveries []TransferDeliveryDTO
|
||||||
for _, del := range e.Deliveries {
|
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{
|
deliveries = append(deliveries, TransferDeliveryDTO{
|
||||||
Id: del.Id,
|
Id: del.Id,
|
||||||
Supplier: SupplierSimpleDTO{
|
Supplier: SupplierSimpleDTO{
|
||||||
@@ -232,11 +256,12 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
|
|||||||
VehiclePlate: del.VehiclePlate,
|
VehiclePlate: del.VehiclePlate,
|
||||||
DriverName: del.DriverName,
|
DriverName: del.DriverName,
|
||||||
DocumentNumber: del.DocumentNumber,
|
DocumentNumber: del.DocumentNumber,
|
||||||
DocumentPath: del.DocumentPath,
|
|
||||||
ShippingCostItem: del.ShippingCostItem,
|
ShippingCostItem: del.ShippingCostItem,
|
||||||
ShippingCostTotal: del.ShippingCostTotal,
|
ShippingCostTotal: del.ShippingCostTotal,
|
||||||
|
Document: document,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return TransferDetailDTO{
|
return TransferDetailDTO{
|
||||||
TransferListDTO: ToTransferListDTO(e),
|
TransferListDTO: ToTransferListDTO(e),
|
||||||
Details: details,
|
Details: details,
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
package transfers
|
package transfers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
"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"
|
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"
|
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
|
||||||
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
|
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)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(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)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
TransferRoutes(router, userService, transferService)
|
TransferRoutes(router, userService, transferService)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"mime/multipart"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
@@ -27,7 +28,7 @@ import (
|
|||||||
type TransferService interface {
|
type TransferService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.StockTransfer, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.StockTransfer, error)
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type transferService struct {
|
type transferService struct {
|
||||||
@@ -42,9 +43,10 @@ type transferService struct {
|
|||||||
SupplierRepo rSupplier.SupplierRepository
|
SupplierRepo rSupplier.SupplierRepository
|
||||||
WarehouseRepo warehouseRepo.WarehouseRepository
|
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||||
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
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{
|
return &transferService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
@@ -57,6 +59,7 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
|
|||||||
SupplierRepo: supplierRepo,
|
SupplierRepo: supplierRepo,
|
||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
|
DocumentSvc: documentSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +75,10 @@ func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("Details").
|
Preload("Details").
|
||||||
Preload("Details.Product").
|
Preload("Details.Product").
|
||||||
Preload("Deliveries.Items").
|
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) {
|
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
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Log.Infof("Retrieved %d transfers", len(transfers))
|
|
||||||
|
|
||||||
return transfers, total, nil
|
return transfers, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
|
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 {
|
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
return s.withRelations(db)
|
return s.withRelations(db)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.Log.Errorf("Error getting StockTransfer ID %d: %+v", id, err)
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
|
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")
|
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
|
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))
|
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())
|
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context())
|
||||||
if err != nil {
|
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")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
|
||||||
}
|
}
|
||||||
movementNumber := fmt.Sprintf("PND-MBU-%05d", seqNum)
|
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 {
|
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 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
s.Log.Infof("Stock transfer created: %+v", entityTransfer.Id)
|
|
||||||
|
|
||||||
var details []*entity.StockTransferDetail
|
var details []*entity.StockTransferDetail
|
||||||
for _, product := range req.Products {
|
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 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
s.Log.Infof("Stock transfer details created for transfer ID: %+v", entityTransfer.Id)
|
|
||||||
|
|
||||||
var deliveries []*entity.StockTransferDelivery
|
var deliveries []*entity.StockTransferDelivery
|
||||||
for _, delivery := range req.Deliveries {
|
for _, delivery := range req.Deliveries {
|
||||||
@@ -224,13 +225,11 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
SupplierId: uint64(delivery.SupplierID),
|
SupplierId: uint64(delivery.SupplierID),
|
||||||
VehiclePlate: delivery.VehiclePlate,
|
VehiclePlate: delivery.VehiclePlate,
|
||||||
DriverName: delivery.DriverName,
|
DriverName: delivery.DriverName,
|
||||||
DocumentPath: "https://tourism.gov.in/sites/default/files/2019-04/dummy-pdf_2.pdf",
|
|
||||||
ShippingCostItem: delivery.DeliveryCostPerItem,
|
ShippingCostItem: delivery.DeliveryCostPerItem,
|
||||||
ShippingCostTotal: delivery.DeliveryCost,
|
ShippingCostTotal: delivery.DeliveryCost,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err := s.StockTransferDeliveryRepo.WithTx(tx).CreateMany(c.Context(), deliveries, nil); err != nil {
|
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
|
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 {
|
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
|
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 {
|
for _, product := range req.Products {
|
||||||
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID))
|
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID))
|
||||||
if err != nil {
|
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")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source product warehouse")
|
||||||
}
|
}
|
||||||
if sourcePW.Quantity < product.ProductQty {
|
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))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock in source warehouse for product ID: %d", product.ProductID))
|
||||||
}
|
}
|
||||||
sourcePW.Quantity -= product.ProductQty
|
sourcePW.Quantity -= product.ProductQty
|
||||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), sourcePW.Id, sourcePW, nil); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
|
|
||||||
|
|
||||||
decreaseLog := &entity.StockLog{
|
decreaseLog := &entity.StockLog{
|
||||||
Decrease: product.ProductQty,
|
Decrease: product.ProductQty,
|
||||||
Notes: "",
|
Notes: "",
|
||||||
LoggableType: entity.LogTypeTransfer,
|
LoggableType: string(utils.StockLogTypeTransfer),
|
||||||
LoggableId: uint(entityTransfer.Id),
|
LoggableId: uint(entityTransfer.Id),
|
||||||
ProductWarehouseId: sourcePW.Id,
|
ProductWarehouseId: sourcePW.Id,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), decreaseLog, nil); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +309,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
|
||||||
)
|
)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
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")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get destination product warehouse")
|
||||||
}
|
}
|
||||||
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -311,29 +324,24 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
ProjectFlockKandangId: &projectFlockKandangID,
|
ProjectFlockKandangId: &projectFlockKandangID,
|
||||||
}
|
}
|
||||||
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
|
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")
|
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
|
destPW.Quantity += product.ProductQty
|
||||||
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), destPW.Id, destPW, nil); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
|
|
||||||
|
|
||||||
increaseLog := &entity.StockLog{
|
increaseLog := &entity.StockLog{
|
||||||
Increase: product.ProductQty,
|
Increase: product.ProductQty,
|
||||||
LoggableType: entity.LogTypeTransfer,
|
LoggableType: string(utils.StockLogTypeTransfer),
|
||||||
LoggableId: uint(entityTransfer.Id),
|
LoggableId: uint(entityTransfer.Id),
|
||||||
Notes: "",
|
Notes: "",
|
||||||
ProductWarehouseId: destPW.Id,
|
ProductWarehouseId: destPW.Id,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), increaseLog, nil); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,7 +351,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
|
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))
|
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) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
|
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")
|
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) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
|
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")
|
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingD
|
|||||||
return MarketingDeliveryProductDTO{
|
return MarketingDeliveryProductDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
MarketingProductId: e.MarketingProductId,
|
MarketingProductId: e.MarketingProductId,
|
||||||
Qty: e.Qty,
|
Qty: e.UsageQty,
|
||||||
UnitPrice: e.UnitPrice,
|
UnitPrice: e.UnitPrice,
|
||||||
TotalWeight: e.TotalWeight,
|
TotalWeight: e.TotalWeight,
|
||||||
AvgWeight: e.AvgWeight,
|
AvgWeight: e.AvgWeight,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package marketing
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -13,11 +14,12 @@ import (
|
|||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
|
||||||
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
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"
|
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"
|
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"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MarketingModule struct{}
|
type MarketingModule struct{}
|
||||||
@@ -31,6 +33,28 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
customerRepo := rCustomer.NewCustomerRepository(db)
|
customerRepo := rCustomer.NewCustomerRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(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
|
// Initialize approval service
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
|
||||||
@@ -42,9 +66,10 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
|
|
||||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc,warehouseRepo,projectFlockKandangRepo, validate)
|
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, warehouseRepo, projectFlockKandangRepo, validate)
|
||||||
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate)
|
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, fifoService, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
// Register routes
|
// Register routes
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ type MarketingDeliveryProductRepository interface {
|
|||||||
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
|
||||||
GetByMarketingProductID(ctx context.Context, marketingProductID 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)
|
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 {
|
type MarketingDeliveryProductRepositoryImpl struct {
|
||||||
@@ -241,3 +244,33 @@ func containsJoin(db *gorm.DB, tableName string) bool {
|
|||||||
joinSQL := statement.SQL.String()
|
joinSQL := statement.SQL.String()
|
||||||
return strings.Contains(joinSQL, "JOIN "+tableName)
|
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 := router.Group("/marketing")
|
||||||
route.Use(m.Auth(userService))
|
route.Use(m.Auth(userService))
|
||||||
|
|
||||||
route.Get("/",m.RequirePermissions(m.P_DeliveryGetAll), deliveryOrdersCtrl.GetAll)
|
route.Get("/", m.RequirePermissions(m.P_DeliveryGetAll), deliveryOrdersCtrl.GetAll)
|
||||||
route.Get("/:id",m.RequirePermissions(m.P_DeliveryGetOne), deliveryOrdersCtrl.GetOne)
|
route.Get("/:id", m.RequirePermissions(m.P_DeliveryGetOne), deliveryOrdersCtrl.GetOne)
|
||||||
route.Delete("/:id",m.RequirePermissions(m.P_SalesOrderDelete), salesOrdersCtrl.DeleteOne)
|
route.Delete("/:id", m.RequirePermissions(m.P_SalesOrderDelete), salesOrdersCtrl.DeleteOne)
|
||||||
|
|
||||||
route.Post("/sales-orders",m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne)
|
route.Post("/sales-orders", m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne)
|
||||||
route.Patch("/sales-orders/:id",m.RequirePermissions(m.P_SalesOrderUpdateOne), salesOrdersCtrl.UpdateOne)
|
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/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"
|
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,12 +30,12 @@ type DeliveryOrdersService interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type deliveryOrdersService struct {
|
type deliveryOrdersService struct {
|
||||||
Log *logrus.Logger
|
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
MarketingRepo marketingRepo.MarketingRepository
|
MarketingRepo marketingRepo.MarketingRepository
|
||||||
MarketingProductRepo marketingRepo.MarketingProductRepository
|
MarketingProductRepo marketingRepo.MarketingProductRepository
|
||||||
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
|
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
FifoSvc commonSvc.FifoService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDeliveryOrdersService(
|
func NewDeliveryOrdersService(
|
||||||
@@ -43,15 +43,16 @@ func NewDeliveryOrdersService(
|
|||||||
marketingProductRepo marketingRepo.MarketingProductRepository,
|
marketingProductRepo marketingRepo.MarketingProductRepository,
|
||||||
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
|
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
|
||||||
approvalSvc commonSvc.ApprovalService,
|
approvalSvc commonSvc.ApprovalService,
|
||||||
|
fifoSvc commonSvc.FifoService,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
) DeliveryOrdersService {
|
) DeliveryOrdersService {
|
||||||
return &deliveryOrdersService{
|
return &deliveryOrdersService{
|
||||||
Log: utils.Log,
|
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
MarketingRepo: marketingRepo,
|
MarketingRepo: marketingRepo,
|
||||||
MarketingProductRepo: marketingProductRepo,
|
MarketingProductRepo: marketingProductRepo,
|
||||||
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
|
FifoSvc: fifoSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +109,6 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get marketings: %+v", err)
|
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
for i := range marketings {
|
for i := range marketings {
|
||||||
@@ -116,7 +116,7 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
|
|||||||
return db.Preload("ActionUser")
|
return db.Preload("ActionUser")
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Warnf("Failed to load approval for marketing %d: %+v", marketings[i].Id, err)
|
continue
|
||||||
}
|
}
|
||||||
marketings[i].LatestApproval = latestApproval
|
marketings[i].LatestApproval = latestApproval
|
||||||
}
|
}
|
||||||
@@ -247,7 +247,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
|
|||||||
itemDeliveryDate = &parsedDate
|
itemDeliveryDate = &parsedDate
|
||||||
}
|
}
|
||||||
|
|
||||||
deliveryProduct.Qty = requestedProduct.Qty
|
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
||||||
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
||||||
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
|
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
|
||||||
deliveryProduct.TotalWeight = requestedProduct.TotalWeight
|
deliveryProduct.TotalWeight = requestedProduct.TotalWeight
|
||||||
@@ -256,7 +256,8 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
|
|||||||
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
|
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
|
||||||
|
|
||||||
if requestedProduct.Qty > 0 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,8 +355,9 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
|
|||||||
itemDeliveryDate = deliveryProduct.DeliveryDate
|
itemDeliveryDate = deliveryProduct.DeliveryDate
|
||||||
}
|
}
|
||||||
|
|
||||||
oldQty := deliveryProduct.Qty
|
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
|
||||||
deliveryProduct.Qty = requestedProduct.Qty
|
|
||||||
|
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
||||||
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
||||||
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
|
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
|
||||||
deliveryProduct.TotalWeight = requestedProduct.TotalWeight
|
deliveryProduct.TotalWeight = requestedProduct.TotalWeight
|
||||||
@@ -363,14 +365,18 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
|
|||||||
deliveryProduct.DeliveryDate = itemDeliveryDate
|
deliveryProduct.DeliveryDate = itemDeliveryDate
|
||||||
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
|
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
|
||||||
|
|
||||||
qtyChange := requestedProduct.Qty - oldQty
|
if requestedProduct.Qty != oldRequestedQty {
|
||||||
if qtyChange > 0 {
|
|
||||||
if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, qtyChange); err != nil {
|
if oldRequestedQty > 0 {
|
||||||
return err
|
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 {
|
if requestedProduct.Qty > 0 {
|
||||||
return err
|
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)
|
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 {
|
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
|
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 err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found")
|
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 {
|
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for warehouse - available: %.2f, requested: %.2f", pw.Quantity, qtyDeliver))
|
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
|
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 {
|
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
|
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
|
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
|
||||||
pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
|
currentUsage, err := deliveryProductRepo.GetUsageQty(ctx, deliveryProduct.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
currentUsage = 0
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pw.Quantity = pw.Quantity + qtyRestore
|
if currentUsage == 0 {
|
||||||
if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil {
|
return nil
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock")
|
}
|
||||||
|
|
||||||
|
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
|
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 _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
|
||||||
mdp := &entity.MarketingDeliveryProduct{
|
mdp := &entity.MarketingDeliveryProduct{
|
||||||
MarketingProductId: old.Id,
|
MarketingProductId: old.Id,
|
||||||
Qty: 0,
|
|
||||||
UnitPrice: 0,
|
UnitPrice: 0,
|
||||||
TotalWeight: 0,
|
TotalWeight: 0,
|
||||||
AvgWeight: 0,
|
AvgWeight: 0,
|
||||||
TotalPrice: 0,
|
TotalPrice: 0,
|
||||||
DeliveryDate: nil,
|
DeliveryDate: nil,
|
||||||
VehicleNumber: rp.VehicleNumber,
|
VehicleNumber: rp.VehicleNumber,
|
||||||
|
UsageQty: 0,
|
||||||
|
PendingQty: 0,
|
||||||
}
|
}
|
||||||
if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil {
|
if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product")
|
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 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))
|
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{
|
marketingDeliveryProduct := &entity.MarketingDeliveryProduct{
|
||||||
MarketingProductId: marketingProduct.Id,
|
MarketingProductId: marketingProduct.Id,
|
||||||
Qty: 0,
|
|
||||||
UnitPrice: 0,
|
UnitPrice: 0,
|
||||||
TotalWeight: 0,
|
TotalWeight: 0,
|
||||||
AvgWeight: 0,
|
AvgWeight: 0,
|
||||||
TotalPrice: 0,
|
TotalPrice: 0,
|
||||||
DeliveryDate: nil,
|
DeliveryDate: nil,
|
||||||
VehicleNumber: rp.VehicleNumber,
|
VehicleNumber: rp.VehicleNumber,
|
||||||
|
UsageQty: 0,
|
||||||
|
PendingQty: 0,
|
||||||
}
|
}
|
||||||
if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil {
|
if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil {
|
||||||
return err
|
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"
|
suppliers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers"
|
||||||
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
|
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
|
||||||
warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses"
|
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
|
// MODULE IMPORTS
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
|
|||||||
products.ProductModule{},
|
products.ProductModule{},
|
||||||
banks.BankModule{},
|
banks.BankModule{},
|
||||||
flocks.FlockModule{},
|
flocks.FlockModule{},
|
||||||
|
productionStandards.ProductionStandardModule{},
|
||||||
// MODULE REGISTRY
|
// MODULE REGISTRY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package chickins
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -9,6 +10,7 @@ import (
|
|||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
|
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
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)
|
projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
|
||||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
||||||
|
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
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)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil {
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil {
|
||||||
panic(fmt.Sprintf("failed to register chickin approval workflow: %v", err))
|
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)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ChickinRoutes(router, userService, chickinService)
|
ChickinRoutes(router, userService, chickinService)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -15,7 +16,9 @@ import (
|
|||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations"
|
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"
|
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"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
@@ -23,6 +26,8 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var chickinUsableKey = fifo.UsableKeyProjectChickin
|
||||||
|
|
||||||
type ChickinService interface {
|
type ChickinService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
|
||||||
@@ -43,9 +48,11 @@ type chickinService struct {
|
|||||||
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
|
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
|
||||||
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
|
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
|
||||||
ProjectChickinDetailRepo repository.ProjectChickinDetailRepository
|
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{
|
return &chickinService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
@@ -57,6 +64,8 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan
|
|||||||
ProjectflockKandangRepo: projectflockkandangRepo,
|
ProjectflockKandangRepo: projectflockkandangRepo,
|
||||||
ProjectflockPopulationRepo: projectflockpopulationRepo,
|
ProjectflockPopulationRepo: projectflockpopulationRepo,
|
||||||
ProjectChickinDetailRepo: projectChickinDetailRepo,
|
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")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
|
|
||||||
|
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
newChikins := make([]*entity.ProjectChickin, 0)
|
newChikins := make([]*entity.ProjectChickin, 0)
|
||||||
|
chickinQtyMap := make(map[uint]float64)
|
||||||
|
|
||||||
for _, chickinReq := range req.ChickinRequests {
|
for idx, chickinReq := range req.ChickinRequests {
|
||||||
|
|
||||||
productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickinReq.ProductWarehouseId, nil)
|
productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickinReq.ProductWarehouseId, nil)
|
||||||
if err != 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))
|
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)
|
availableQty := productWarehouse.Quantity
|
||||||
if err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to calculate available quantity for product warehouse %d", chickinReq.ProductWarehouseId))
|
|
||||||
}
|
|
||||||
|
|
||||||
if availableQty <= 0 {
|
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{
|
newChickin := &entity.ProjectChickin{
|
||||||
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
||||||
ChickInDate: chickinDate,
|
ChickInDate: chickinDate,
|
||||||
UsageQty: 0,
|
UsageQty: 0,
|
||||||
PendingUsageQty: availableQty,
|
PendingUsageQty: 0,
|
||||||
ProductWarehouseId: chickinReq.ProductWarehouseId,
|
ProductWarehouseId: chickinReq.ProductWarehouseId,
|
||||||
Notes: chickinReq.Note,
|
Notes: chickinReq.Note,
|
||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
}
|
}
|
||||||
|
|
||||||
newChikins = append(newChikins, newChickin)
|
newChikins = append(newChikins, newChickin)
|
||||||
|
chickinQtyMap[uint(idx)] = availableQty
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(newChikins) == 0 {
|
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 {
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
|
||||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
|
||||||
|
|
||||||
if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil {
|
if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
|
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)
|
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, projectFlockKandang.Id, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval")
|
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
|
var approvalAction entity.ApprovalAction
|
||||||
if isFirstTime {
|
if isFirstTime {
|
||||||
approvalAction = entity.ApprovalActionCreated
|
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 {
|
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 err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
||||||
@@ -311,54 +335,6 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
return nil
|
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) {
|
func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -387,11 +363,10 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, id := range approvableIDs {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, id, nil)
|
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, id, nil)
|
||||||
|
|
||||||
if err != 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))
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
||||||
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
|
||||||
|
|
||||||
for _, approvableID := range approvableIDs {
|
for _, approvableID := range approvableIDs {
|
||||||
if _, err := approvalSvc.CreateApproval(
|
if _, err := approvalSvc.CreateApproval(
|
||||||
@@ -472,9 +446,9 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
}
|
}
|
||||||
|
|
||||||
pfkID := approvableID
|
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 {
|
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 {
|
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
|
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
|
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 {
|
for _, chickin := range chickins {
|
||||||
|
|
||||||
if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) {
|
if err := s.ReleaseChickinStocks(c.Context(), dbTransaction, &chickin, actorID); err != nil {
|
||||||
updates := map[string]any{"qty": gorm.Expr("qty + ?", chickin.PendingUsageQty)}
|
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 {
|
warehouseDeltas := make(map[uint]float64)
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty
|
||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId))
|
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 fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to restore product warehouse quantity for chickin %d", chickin.Id))
|
return err
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil {
|
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)
|
products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
|
||||||
if err == nil && len(products) > 0 {
|
if err == nil && len(products) > 0 {
|
||||||
existingPW := &products[0]
|
existingPW := &products[0]
|
||||||
// Update project_flock_kandang_id if not already set
|
|
||||||
if existingPW.ProjectFlockKandangId == nil && projectFlockKandangId != nil {
|
if existingPW.ProjectFlockKandangId == nil && projectFlockKandangId != nil {
|
||||||
existingPW.ProjectFlockKandangId = projectFlockKandangId
|
existingPW.ProjectFlockKandangId = projectFlockKandangId
|
||||||
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).UpdateOne(ctx.Context(), existingPW.Id, existingPW, nil); err != nil {
|
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,
|
WarehouseId: warehouseId,
|
||||||
ProjectFlockKandangId: projectFlockKandangId,
|
ProjectFlockKandangId: projectFlockKandangId,
|
||||||
Quantity: 0,
|
Quantity: 0,
|
||||||
// CreatedBy: actorID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil {
|
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")
|
return fmt.Errorf("invalid target product warehouse")
|
||||||
}
|
}
|
||||||
|
|
||||||
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
|
||||||
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
|
||||||
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
|
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
|
||||||
|
|
||||||
|
var totalQuantityAdded float64
|
||||||
|
|
||||||
for _, chickin := range chickins {
|
for _, chickin := range chickins {
|
||||||
|
|
||||||
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id)
|
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id)
|
||||||
@@ -604,34 +567,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
quantityToConvert := chickin.PendingUsageQty
|
quantityToConvert := chickin.UsageQty
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
population := &entity.ProjectFlockPopulation{
|
population := &entity.ProjectFlockPopulation{
|
||||||
ProjectChickinId: chickin.Id,
|
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 {
|
if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalQuantityAdded += quantityToConvert
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalQuantityAdded > 0 {
|
||||||
|
if err := s.ProductWarehouseRepo.AdjustQuantities(ctx.Context(), map[uint]float64{
|
||||||
|
targetPW.Id: totalQuantityAdded,
|
||||||
|
}, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return dbTransaction
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("failed to update target product warehouse quantity: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
availableQty := productWarehouse.Quantity
|
||||||
|
|
||||||
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
|
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
|
||||||
|
|
||||||
var totalPendingQty float64
|
var totalPendingQty float64
|
||||||
|
|
||||||
for _, chickin := range projectFlockKandang.Chickins {
|
for _, chickin := range projectFlockKandang.Chickins {
|
||||||
@@ -568,15 +569,8 @@ func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehous
|
|||||||
availableQty = 0
|
availableQty = 0
|
||||||
}
|
}
|
||||||
} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
|
} 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)
|
var totalPendingQty float64
|
||||||
if err == nil {
|
|
||||||
for _, pop := range populations {
|
|
||||||
totalPopulation += pop.TotalQty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, chickin := range projectFlockKandang.Chickins {
|
for _, chickin := range projectFlockKandang.Chickins {
|
||||||
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
|
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 {
|
if availableQty < 0 {
|
||||||
availableQty = 0
|
availableQty = 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package purchases
|
package purchases
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
@@ -41,6 +42,11 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
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 {
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPurchase, utils.PurchaseApprovalSteps); err != nil {
|
||||||
panic(fmt.Sprintf("failed to register purchase approval workflow: %v", err))
|
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,
|
approvalService,
|
||||||
expenseRealizationRepo,
|
expenseRealizationRepo,
|
||||||
projectFlockKandangRepository,
|
projectFlockKandangRepository,
|
||||||
|
documentSvc,
|
||||||
validate,
|
validate,
|
||||||
)
|
)
|
||||||
expenseBridge := service.NewExpenseBridge(
|
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)
|
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
|
salesAmount := totalWeightKg * mdp.UnitPrice
|
||||||
|
|
||||||
var hpp float64
|
var hpp float64
|
||||||
@@ -78,7 +78,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
|
|||||||
AgingDays: agingDays,
|
AgingDays: agingDays,
|
||||||
DoNumber: doNumber,
|
DoNumber: doNumber,
|
||||||
MarketingType: getMarketingType(mdp),
|
MarketingType: getMarketingType(mdp),
|
||||||
Qty: mdp.Qty,
|
Qty: mdp.UsageQty,
|
||||||
AverageWeightKg: mdp.AvgWeight,
|
AverageWeightKg: mdp.AvgWeight,
|
||||||
TotalWeightKg: totalWeightKg,
|
TotalWeightKg: totalWeightKg,
|
||||||
SalesPricePerKg: mdp.UnitPrice,
|
SalesPricePerKg: mdp.UnitPrice,
|
||||||
@@ -194,8 +194,8 @@ func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, ca
|
|||||||
totalHppAmount := int64(0)
|
totalHppAmount := int64(0)
|
||||||
|
|
||||||
for _, mdp := range mdps {
|
for _, mdp := range mdps {
|
||||||
calculatedTotalWeight := mdp.Qty * mdp.AvgWeight
|
calculatedTotalWeight := mdp.UsageQty * mdp.AvgWeight
|
||||||
totalQty += int(mdp.Qty)
|
totalQty += int(mdp.UsageQty)
|
||||||
totalWeightKg += calculatedTotalWeight
|
totalWeightKg += calculatedTotalWeight
|
||||||
totalSalesAmount += int64(calculatedTotalWeight * mdp.UnitPrice)
|
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 {
|
func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, category string) float64 {
|
||||||
totalCost := s.getTotalProjectCost(ctx, projectFlockID)
|
totalCost := s.getTotalProjectCost(ctx, projectFlockID)
|
||||||
if totalCost == 0 {
|
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
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import (
|
|||||||
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
|
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
|
||||||
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
|
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
|
||||||
purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases"
|
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"
|
ssoModule "gitlab.com/mbugroup/lti-api.git/internal/modules/sso"
|
||||||
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
|
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
|
||||||
repports "gitlab.com/mbugroup/lti-api.git/internal/modules/repports"
|
|
||||||
// MODULE IMPORTS
|
// MODULE IMPORTS
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,7 +43,8 @@ func Routes(app *fiber.App, db *gorm.DB) {
|
|||||||
expenses.ExpenseModule{},
|
expenses.ExpenseModule{},
|
||||||
ssoModule.Module{},
|
ssoModule.Module{},
|
||||||
closings.ClosingModule{},
|
closings.ClosingModule{},
|
||||||
repports.RepportModule{},
|
repports.RepportModule{},
|
||||||
|
|
||||||
// MODULE REGISTRY
|
// MODULE REGISTRY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ type StockLogType string
|
|||||||
const (
|
const (
|
||||||
StockLogTypeAdjustment StockLogType = "ADJUSTMENT"
|
StockLogTypeAdjustment StockLogType = "ADJUSTMENT"
|
||||||
StockLogTypeTransfer StockLogType = "TRANSFER"
|
StockLogTypeTransfer StockLogType = "TRANSFER"
|
||||||
|
StockLogTypeMarketing StockLogType = "MARKETING"
|
||||||
|
StockLogTypeChikin StockLogType = "CHICKIN"
|
||||||
)
|
)
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
@@ -314,6 +316,23 @@ var ExpenseApprovalSteps = map[approvalutils.ApprovalStep]string{
|
|||||||
ExpenseStepSelesai: "Selesai",
|
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
|
// Validators
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
@@ -448,7 +467,7 @@ func IsValidExpenseCategory(v string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// example use
|
// e xample use
|
||||||
|
|
||||||
// Recording helper
|
// Recording helper
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package fifo
|
package fifo
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
|
||||||
|
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
|
||||||
|
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user