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:
Hafizh A. Y.
2025-12-27 07:40:55 +00:00
54 changed files with 1921 additions and 474 deletions
+19 -6
View File
@@ -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
@@ -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;
@@ -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;
@@ -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);
@@ -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;
@@ -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);
@@ -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);
+6 -7
View File
@@ -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"`
} }
+19
View File
@@ -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"`
}
+1
View File
@@ -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"`
} }
-10
View File
@@ -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"`
+18 -17
View File
@@ -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"`
}
+1
View File
@@ -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 {
+14 -7
View File
@@ -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 {
+7 -1
View File
@@ -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,
+11 -1
View File
@@ -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,
+29 -4
View File
@@ -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
}
+9 -6
View File
@@ -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
@@ -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)
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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"`
}
+2
View File
@@ -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
} }
+31 -2
View File
@@ -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(&currentUsage).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 })
}
@@ -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
} }
+7
View File
@@ -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
} }
+3 -2
View File
@@ -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
} }
+20 -1
View File
@@ -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
+3 -1
View File
@@ -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"
) )