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
for key, cfg := range configs {
selectStmt := fmt.Sprintf(
"%s AS id, %s AS available_qty, %s AS created_at",
cfg.Columns.ID,
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
cfg.Columns.CreatedAt,
)
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
var selectStmt string
if usesNumericTime {
selectStmt = fmt.Sprintf(
"%s AS id, %s AS available_qty, '1970-01-01 00:00:00 UTC'::timestamp AS created_at",
cfg.Columns.ID,
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
)
} else {
selectStmt = fmt.Sprintf(
"%s AS id, %s AS available_qty, %s AS created_at",
cfg.Columns.ID,
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
cfg.Columns.CreatedAt,
)
}
var rows []struct {
ID uint
@@ -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
import (
"database/sql"
"time"
"gorm.io/gorm"
@@ -13,8 +12,6 @@ type Expense struct {
SupplierId uint64 `gorm:""`
Category string `gorm:"type:varchar(50);not null"`
PoNumber string `gorm:"type:varchar(50)"`
DocumentPath sql.NullString `gorm:"type:json"`
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"`
RealizationDate time.Time `gorm:"type:date;column:realization_date"`
TransactionDate time.Time `gorm:"type:date;not null"`
Notes string `gorm:"type:text;column:notes"`
@@ -23,8 +20,10 @@ type Expense struct {
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
Documents []Document `gorm:"foreignKey:DocumentableId;references:Id"`
RealizationDocuments []Document `gorm:"foreignKey:DocumentableId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"`
}
@@ -5,15 +5,20 @@ import (
)
type MarketingDeliveryProduct struct {
Id uint `gorm:"primaryKey;autoIncrement"`
MarketingProductId uint `gorm:"uniqueIndex;not null"`
Qty float64 `gorm:"type:numeric(15,3)"`
UnitPrice float64 `gorm:"type:numeric(15,3)"`
TotalWeight float64 `gorm:"type:numeric(15,3)"`
AvgWeight float64 `gorm:"type:numeric(15,3)"`
TotalPrice float64 `gorm:"type:numeric(15,3)"`
DeliveryDate *time.Time `gorm:"type:timestamptz"`
VehicleNumber string `gorm:"type:varchar(50)"`
Id uint `gorm:"primaryKey;autoIncrement"`
MarketingProductId uint `gorm:"uniqueIndex;not null"`
ProductWarehouseId uint `gorm:"not null"`
UnitPrice float64 `gorm:"type:numeric(15,3)"`
TotalWeight float64 `gorm:"type:numeric(15,3)"`
AvgWeight float64 `gorm:"type:numeric(15,3)"`
TotalPrice float64 `gorm:"type:numeric(15,3)"`
DeliveryDate *time.Time `gorm:"type:timestamptz"`
VehicleNumber string `gorm:"type:varchar(50)"`
// FIFO Fields
UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
PendingQty float64 `gorm:"type:numeric(15,3);default:0;not null"`
CreatedAt *time.Time `gorm:"type:timestamptz;not null"`
MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"`
}
+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"`
Deliveries []StockTransferDelivery `gorm:"foreignKey:StockTransferId"`
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"
const (
LogTypeAdjustment = "ADJUSTMENT"
LogTypeTransfer = "TRANSFER"
)
const (
TransactionTypeIncrease = "INCREASE"
TransactionTypeDecrease = "DECREASE"
)
type StockLog struct {
Id uint `gorm:"primaryKey;column:id"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null;index"`
+18 -17
View File
@@ -4,20 +4,21 @@ import "time"
// DETAIL EKSPEDISI
type StockTransferDelivery struct {
Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferId uint64
SupplierId uint64
VehiclePlate string
DriverName string
DocumentNumber string
DocumentPath string
ShippingCostItem float64
ShippingCostTotal float64
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// Relations
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
}
Id uint64 `gorm:"primaryKey;autoIncrement"`
StockTransferId uint64
SupplierId uint64
VehiclePlate string
DriverName string
DocumentNumber string
DocumentPath string
ShippingCostItem float64
ShippingCostTotal float64
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// Relations
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
Supplier *Supplier `gorm:"foreignKey:SupplierId"`
Items []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDeliveryId"`
Documents []Document `gorm:"foreignKey:DocumentableId;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}
+1
View File
@@ -77,6 +77,7 @@ const (
P_DeliveryGetAll = "lti.marketing.delivery_order.list"
P_DeliveryGetOne = "lti.marketing.delivery_order.detail"
P_DeliveryUpdateOne = "lti.marketing.delivery_order.update"
P_DeliveryCreateOne = "lti.marketing.delivery_order.Create"
P_SalesOrderDelete = "lti.marketing.sales_order.delete"
P_SalesOrderApproval = "lti.marketing.sales_order.approve"
P_SalesOrderCreateOne = "lti.marketing.sales_order.create"
@@ -64,7 +64,7 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
DoNumber: doNumber,
Product: product,
Customer: customer,
Qty: e.Qty,
Qty: e.UsageQty, // Show allocated quantity from FIFO
Weight: e.TotalWeight,
AvgWeight: e.AvgWeight,
Price: e.UnitPrice,
@@ -783,7 +783,7 @@ func splitStockLogs(rows []stockLogSapronakRow, refFn func(stockLogSapronakRow)
}
func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeAdjustment, false)
rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeAdjustment), false)
if err != nil {
return nil, nil, err
}
@@ -792,7 +792,7 @@ func (r *ClosingRepositoryImpl) FetchSapronakAdjustments(ctx context.Context, ka
}
func (r *ClosingRepositoryImpl) FetchSapronakTransfers(ctx context.Context, kandangID uint) (map[uint][]SapronakDetailRow, map[uint][]SapronakDetailRow, error) {
rows, err := r.fetchStockLogs(ctx, kandangID, entity.LogTypeTransfer, true)
rows, err := r.fetchStockLogs(ctx, kandangID, string(utils.StockLogTypeTransfer), true)
if err != nil {
return nil, nil, err
}
@@ -712,13 +712,13 @@ func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlo
)
for _, product := range deliveryProducts {
if product.Qty == 0 {
if product.UsageQty == 0 {
continue
}
projectFlockKandang := product.MarketingProduct.ProductWarehouse.ProjectFlockKandang
ageWeeks := dto.CalculateAgeFromChickinDataProduksi(projectFlockKandang, product.DeliveryDate)
totalAgeWeeks += float64(ageWeeks) * product.Qty
totalQty += product.Qty
totalAgeWeeks += float64(ageWeeks) * product.UsageQty
totalQty += product.UsageQty
}
if totalQty == 0 {
+14 -7
View File
@@ -1,7 +1,6 @@
package dto
import (
"encoding/json"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -41,8 +40,8 @@ type ExpenseListDTO struct {
type ExpenseDetailDTO struct {
ExpenseBaseDTO
Documents []DocumentDTO `json:"documents,omitempty"`
RealizationDocs []DocumentDTO `json:"realization_docs,omitempty"`
Documents []DocumentDTO `json:"documents"`
RealizationDocs []DocumentDTO `json:"realization_docs"`
Kandangs []KandangGroupDTO `json:"kandangs,omitempty"`
TotalPengajuan float64 `json:"total_pengajuan"`
TotalRealisasi float64 `json:"total_realisasi"`
@@ -179,12 +178,20 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
var pengajuans []ExpenseNonstockDTO
var realisasi []ExpenseRealizationDTO
if e.DocumentPath.Valid && e.DocumentPath.String != "" {
json.Unmarshal([]byte(e.DocumentPath.String), &documents)
// Map documents from Document service
for _, doc := range e.Documents {
documents = append(documents, DocumentDTO{
ID: uint64(doc.Id),
Path: doc.Path,
})
}
if e.RealizationDocumentPath.Valid && e.RealizationDocumentPath.String != "" {
json.Unmarshal([]byte(e.RealizationDocumentPath.String), &realizationDocs)
// Map realization documents from Document service
for _, doc := range e.RealizationDocuments {
realizationDocs = append(realizationDocs, DocumentDTO{
ID: uint64(doc.Id),
Path: doc.Path,
})
}
if len(e.Nonstocks) > 0 {
+7 -1
View File
@@ -1,6 +1,7 @@
package expenses
import (
"context"
"fmt"
"github.com/go-playground/validator/v10"
@@ -31,15 +32,20 @@ func (ExpenseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
approvalRepo := commonRepo.NewApprovalRepository(db)
realizationRepo := rExpense.NewExpenseRealizationRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
documentRepo := commonRepo.NewDocumentRepository(db)
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
if err != nil {
panic(fmt.Sprintf("failed to create document service: %v", err))
}
// Register workflow steps for EXPENSES approval
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowExpense, utils.ExpenseApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register expense approval workflow: %v", err))
}
expenseService := sExpense.NewExpenseService(expenseRepo, supplierRepo, nonstockRepo, approvalSvc, realizationRepo, projectFlockKandangRepo, validate)
expenseService := sExpense.NewExpenseService(expenseRepo, supplierRepo, nonstockRepo, approvalSvc, realizationRepo, projectFlockKandangRepo, documentSvc, validate)
userService := sUser.NewUserService(userRepo, validate)
ExpenseRoutes(router, userService, expenseService)
@@ -2,11 +2,8 @@ package service
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"mime/multipart"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
@@ -49,9 +46,10 @@ type expenseService struct {
ApprovalSvc commonSvc.ApprovalService
RealizationRepository repository.ExpenseRealizationRepository
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
DocumentSvc commonSvc.DocumentService
}
func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierRepo.SupplierRepository, nonstockRepo nonstockRepo.NonstockRepository, approvalSvc commonSvc.ApprovalService, realizationRepo repository.ExpenseRealizationRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) ExpenseService {
func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierRepo.SupplierRepository, nonstockRepo nonstockRepo.NonstockRepository, approvalSvc commonSvc.ApprovalService, realizationRepo repository.ExpenseRealizationRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, validate *validator.Validate) ExpenseService {
return &expenseService{
Log: utils.Log,
Validate: validate,
@@ -61,6 +59,7 @@ func NewExpenseService(repo repository.ExpenseRepository, supplierRepo supplierR
ApprovalSvc: approvalSvc,
RealizationRepository: realizationRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
DocumentSvc: documentSvc,
}
}
@@ -72,7 +71,13 @@ func (s expenseService) withRelations(db *gorm.DB) *gorm.DB {
Preload("Nonstocks.Realization").
Preload("Nonstocks.ProjectFlockKandang.Kandang.Location").
Preload("Nonstocks.Kandang").
Preload("Nonstocks.Kandang.Location")
Preload("Nonstocks.Kandang.Location").
Preload("Documents", func(db *gorm.DB) *gorm.DB {
return db.Where("documentable_type = ?", string(utils.DocumentableTypeExpense))
}).
Preload("RealizationDocuments", func(db *gorm.DB) *gorm.DB {
return db.Where("documentable_type = ?", string(utils.DocumentableTypeExpenseRealization))
})
}
func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expenseDto.ExpenseListDTO, int64, error) {
@@ -269,9 +274,23 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create initial approval")
}
if len(req.Documents) > 0 {
if err := s.processDocuments(c, expenseRepoTx, uint(expense.Id), req.Documents, false); err != nil {
return err
if s.DocumentSvc != nil && len(req.Documents) > 0 {
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
for idx, file := range req.Documents {
documentFiles = append(documentFiles, commonSvc.DocumentFile{
File: file,
Type: string(utils.DocumentTypeExpense),
Index: &idx,
})
}
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: string(utils.DocumentableTypeExpense),
DocumentableID: expense.Id,
CreatedBy: &createdByUint,
Files: documentFiles,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense documents")
}
}
@@ -527,9 +546,23 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
}
}
if len(req.Documents) > 0 {
if err := s.processDocuments(c, expenseRepoTx, id, req.Documents, false); err != nil {
return err
if s.DocumentSvc != nil && len(req.Documents) > 0 {
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
for idx, file := range req.Documents {
documentFiles = append(documentFiles, commonSvc.DocumentFile{
File: file,
Type: string(utils.DocumentTypeExpense),
Index: &idx,
})
}
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: string(utils.DocumentableTypeExpense),
DocumentableID: uint64(id),
CreatedBy: &actorID,
Files: documentFiles,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense documents")
}
}
@@ -658,9 +691,24 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
}
if len(req.Documents) > 0 {
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
return err
if s.DocumentSvc != nil && len(req.Documents) > 0 {
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
for idx, file := range req.Documents {
documentFiles = append(documentFiles, commonSvc.DocumentFile{
File: file,
Type: string(utils.DocumentTypeExpenseRealization),
Index: &idx,
})
}
actorID := uint(1) // TODO: replace with authenticated user id
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: string(utils.DocumentableTypeExpenseRealization),
DocumentableID: uint64(expenseID),
CreatedBy: &actorID,
Files: documentFiles,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense realization documents")
}
}
@@ -833,9 +881,24 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
}
}
if len(req.Documents) > 0 {
if err := s.processDocuments(c, expenseRepoTx, expenseID, req.Documents, true); err != nil {
return err
if s.DocumentSvc != nil && len(req.Documents) > 0 {
documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents))
for idx, file := range req.Documents {
documentFiles = append(documentFiles, commonSvc.DocumentFile{
File: file,
Type: string(utils.DocumentTypeExpenseRealization),
Index: &idx,
})
}
actorID := uint(1) // TODO: replace with authenticated user id
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: string(utils.DocumentableTypeExpenseRealization),
DocumentableID: uint64(expenseID),
CreatedBy: &actorID,
Files: documentFiles,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to upload expense realization documents")
}
}
@@ -870,79 +933,6 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
return responseDTO, nil
}
func (s *expenseService) processDocuments(ctx *fiber.Ctx, expenseRepoTx repository.ExpenseRepository, expenseID uint, documents []*multipart.FileHeader, isRealization bool) error {
if len(documents) == 0 {
return nil
}
var existingDocuments []expenseDto.DocumentDTO
var fieldName string
if isRealization {
fieldName = "realization_document_path"
} else {
fieldName = "document_path"
}
expense, err := expenseRepoTx.GetByID(ctx.Context(), expenseID, nil)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense for document processing")
}
} else {
var documentField sql.NullString
if isRealization {
documentField = expense.RealizationDocumentPath
} else {
documentField = expense.DocumentPath
}
if documentField.Valid && documentField.String != "" {
if err := json.Unmarshal([]byte(documentField.String), &existingDocuments); err != nil {
existingDocuments = []expenseDto.DocumentDTO{}
}
}
}
var startID uint64 = 1
if len(existingDocuments) > 0 {
maxID := uint64(0)
for _, doc := range existingDocuments {
if doc.ID > maxID {
maxID = doc.ID
}
}
startID = maxID + 1
}
for i, doc := range documents {
documentPath := doc.Filename
document := expenseDto.DocumentDTO{
ID: startID + uint64(i),
Path: documentPath,
}
existingDocuments = append(existingDocuments, document)
}
documentJSON, err := json.Marshal(existingDocuments)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
}
if err := expenseRepoTx.PatchOne(ctx.Context(), expenseID, map[string]interface{}{
fieldName: string(documentJSON),
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
}
return nil
}
func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error {
if err := commonSvc.EnsureRelations(ctx.Context(),
@@ -951,62 +941,40 @@ func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, document
return err
}
if err := s.Repository.DB().WithContext(ctx.Context()).Transaction(func(tx *gorm.DB) error {
expenseRepoTx := repository.NewExpenseRepository(tx)
if s.DocumentSvc == nil {
return fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
}
expense, err := expenseRepoTx.GetByID(ctx.Context(), expenseID, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense for document deletion")
// Verify document exists and belongs to the expense
var documentableType string
if isRealization {
documentableType = string(utils.DocumentableTypeExpenseRealization)
} else {
documentableType = string(utils.DocumentableTypeExpense)
}
documents, err := s.DocumentSvc.ListByTarget(ctx.Context(), documentableType, uint64(expenseID))
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve documents")
}
documentFound := false
var documentIDsToDelete []uint
for _, doc := range documents {
if uint64(doc.Id) == documentID {
documentFound = true
documentIDsToDelete = append(documentIDsToDelete, doc.Id)
break
}
}
var existingDocuments []expenseDto.DocumentDTO
var fieldName string
if !documentFound {
return fiber.NewError(fiber.StatusNotFound, "Document not found")
}
if isRealization {
fieldName = "realization_document_path"
if expense.RealizationDocumentPath.Valid && expense.RealizationDocumentPath.String != "" {
if err := json.Unmarshal([]byte(expense.RealizationDocumentPath.String), &existingDocuments); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to parse existing realization documents")
}
}
} else {
fieldName = "document_path"
if expense.DocumentPath.Valid && expense.DocumentPath.String != "" {
if err := json.Unmarshal([]byte(expense.DocumentPath.String), &existingDocuments); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to parse existing documents")
}
}
}
var updatedDocuments []expenseDto.DocumentDTO
documentFound := false
for _, doc := range existingDocuments {
if doc.ID == documentID {
documentFound = true
continue
}
updatedDocuments = append(updatedDocuments, doc)
}
if !documentFound {
return fiber.NewError(fiber.StatusNotFound, "Document not found")
}
documentJSON, err := json.Marshal(updatedDocuments)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
}
if err := expenseRepoTx.PatchOne(ctx.Context(), expenseID, map[string]interface{}{
fieldName: string(documentJSON),
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to save documents")
}
return nil
}); err != nil {
return err
// Delete document from database and storage
if err := s.DocumentSvc.DeleteDocuments(ctx.Context(), documentIDsToDelete, true); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete document")
}
return nil
@@ -70,7 +70,7 @@ func (s *adjustmentService) GetOne(c *fiber.Ctx, id uint) (*entity.StockLog, err
return nil, err
}
if stockLog.LoggableType != entity.LogTypeAdjustment {
if stockLog.LoggableType != string(utils.StockLogTypeAdjustment) {
return nil, fiber.NewError(fiber.StatusNotFound, "Adjustment not found")
}
@@ -97,7 +97,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
return nil, fiber.NewError(fiber.StatusBadRequest, "Quantity must be greater than zero")
}
transactionType := strings.ToUpper(req.TransactionType)
if transactionType != entity.TransactionTypeIncrease && transactionType != entity.TransactionTypeDecrease {
if transactionType != string(utils.StockLogTransactionTypeIncrease) && transactionType != string(utils.StockLogTransactionTypeDecrease) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction type")
}
@@ -154,14 +154,14 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
afterQuantity := productWarehouse.Quantity
newLog := &entity.StockLog{
// TransactionType: transactionType,
LoggableType: entity.LogTypeAdjustment,
LoggableType: string(utils.StockLogTypeAdjustment),
LoggableId: 0,
Notes: req.Note,
ProductWarehouseId: productWarehouse.Id,
CreatedBy: actorID, // TODO: should Get from auth middleware
}
if transactionType == entity.TransactionTypeIncrease {
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
afterQuantity += req.Quantity
newLog.Increase = afterQuantity
} else {
@@ -248,7 +248,7 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
db = s.withRelations(db)
db = db.Where("loggable_type = ?", entity.LogTypeAdjustment)
db = db.Where("loggable_type = ?", string(utils.StockLogTypeAdjustment))
if query.TransactionType != "" {
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
@@ -68,7 +68,7 @@ func (u *TransferController) GetOne(c *fiber.Ctx) error {
Code: fiber.StatusOK,
Status: "success",
Message: "Get transfer successfully",
Data: dto.ToTransferListDTO(*result),
Data: dto.ToTransferDetailDTO(*result),
})
}
@@ -80,15 +80,19 @@ func (u *TransferController) CreateOne(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
// ambil file
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
_ = form.File["documents"]
// todo: tunggu ada aws baru proses
result, err := u.TransferService.CreateOne(c, &req)
files := form.File["documents"]
if len(files) != len(req.Deliveries) {
return fiber.NewError(fiber.StatusBadRequest,
fiber.NewError(fiber.StatusBadRequest, "Jumlah dokumen harus sama dengan jumlah deliveries").Message)
}
result, err := u.TransferService.CreateOne(c, &req, files)
if err != nil {
return err
}
@@ -98,6 +102,6 @@ func (u *TransferController) CreateOne(c *fiber.Ctx) error {
Code: fiber.StatusCreated,
Status: "success",
Message: "Create transfer successfully",
Data: dto.ToTransferListDTO(*result),
Data: dto.ToTransferDetailDTO(*result),
})
}
@@ -7,8 +7,6 @@ import (
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type TransferRelationDTO struct {
Id uint64 `json:"id"`
TransferReason string `json:"transfer_reason"`
@@ -17,7 +15,6 @@ type TransferRelationDTO struct {
DestinationWarehouse *WarehouseDetailDTO `json:"destination_warehouse,omitempty"`
}
// Only id and name for warehouse simple view
type WarehouseSimpleDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
@@ -43,6 +40,14 @@ type SupplierSimpleDTO struct {
Name string `json:"name"`
}
type DocumentDTO struct {
Id uint `json:"id"`
Path string `json:"path"`
Name string `json:"name"`
Ext string `json:"ext"`
Size float64 `json:"size"`
}
type WarehouseDetailDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
@@ -65,24 +70,22 @@ type TransferDetailDTO struct {
Deliveries []TransferDeliveryDTO `json:"deliveries"`
}
// Detail produk
type TransferDetailItemDTO struct {
Id uint64 `json:"id"`
Proudct ProductSimpleDTO `json:"product"`
Product ProductSimpleDTO `json:"product"`
Quantity float64 `json:"quantity"`
}
// Delivery ekspedisi
type TransferDeliveryDTO struct {
Id uint64 `json:"id"`
Supplier SupplierSimpleDTO `json:"supplier"`
VehiclePlate string `json:"vehicle_plate"`
DriverName string `json:"driver_name"`
DocumentNumber string `json:"document_number"`
DocumentPath string `json:"document_path"`
ShippingCostItem float64 `json:"shipping_cost_item"`
ShippingCostTotal float64 `json:"shipping_cost_total"`
Items []TransferDeliveryItemDTO `json:"items"`
Document *DocumentDTO `json:"document,omitempty"`
}
type TransferDeliveryItemDTO struct {
@@ -91,10 +94,7 @@ type TransferDeliveryItemDTO struct {
Quantity float64 `json:"quantity"`
}
// === Mapper Functions ===
func ToTransferRelationDTO(e entity.StockTransfer) TransferRelationDTO {
var sourceWarehouse *WarehouseDetailDTO
if e.FromWarehouse != nil && e.FromWarehouse.Id != 0 {
sourceWarehouse = toWarehouseDetailDTO(e.FromWarehouse)
@@ -140,7 +140,7 @@ func toWarehouseDetailDTO(w *entity.Warehouse) *WarehouseDetailDTO {
Id: w.Id,
Name: w.Name,
Location: toLocationDTO(w.Location),
Area: toAreaDTO(&w.Area), // Ambil area langsung dari warehouse (area_id)
Area: toAreaDTO(&w.Area),
}
}
@@ -150,22 +150,21 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
mapped := userDTO.ToUserRelationDTO(*e.CreatedUser)
createdUser = &mapped
}
// Map details
var details []TransferDetailItemDTO
for _, d := range e.Details {
details = append(details, TransferDetailItemDTO{
Id: d.Id,
Proudct: ProductSimpleDTO{
Product: ProductSimpleDTO{
Id: d.Product.Id,
Name: d.Product.Name,
},
Quantity: d.Quantity,
})
}
// Map deliveries
var deliveries []TransferDeliveryDTO
for _, del := range e.Deliveries {
// Map delivery items
var items []TransferDeliveryItemDTO
for _, item := range del.Items {
items = append(items, TransferDeliveryItemDTO{
@@ -174,6 +173,19 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
Quantity: item.Quantity,
})
}
var document *DocumentDTO
if len(del.Documents) > 0 {
doc := del.Documents[0] // Take first document
document = &DocumentDTO{
Id: doc.Id,
Path: doc.Path,
Name: doc.Name,
Ext: doc.Ext,
Size: doc.Size,
}
}
deliveries = append(deliveries, TransferDeliveryDTO{
Id: del.Id,
Supplier: SupplierSimpleDTO{
@@ -183,12 +195,13 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO {
VehiclePlate: del.VehiclePlate,
DriverName: del.DriverName,
DocumentNumber: del.DocumentNumber,
DocumentPath: del.DocumentPath,
ShippingCostItem: del.ShippingCostItem,
ShippingCostTotal: del.ShippingCostTotal,
Items: items,
Document: document,
})
}
return TransferListDTO{
TransferRelationDTO: ToTransferRelationDTO(e),
CreatedUser: createdUser,
@@ -208,21 +221,32 @@ func ToTransferListDTOs(e []entity.StockTransfer) []TransferListDTO {
}
func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
// Map details
var details []TransferDetailItemDTO
for _, d := range e.Details {
details = append(details, TransferDetailItemDTO{
Id: d.Id,
Proudct: ProductSimpleDTO{
Product: ProductSimpleDTO{
Id: d.Product.Id,
Name: d.Product.Name,
},
Quantity: d.Quantity,
})
}
// Map deliveries
var deliveries []TransferDeliveryDTO
for _, del := range e.Deliveries {
var document *DocumentDTO
if len(del.Documents) > 0 {
doc := del.Documents[0] // Take first document
document = &DocumentDTO{
Id: doc.Id,
Path: doc.Path,
Name: doc.Name,
Ext: doc.Ext,
Size: doc.Size,
}
}
deliveries = append(deliveries, TransferDeliveryDTO{
Id: del.Id,
Supplier: SupplierSimpleDTO{
@@ -232,11 +256,12 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO {
VehiclePlate: del.VehiclePlate,
DriverName: del.DriverName,
DocumentNumber: del.DocumentNumber,
DocumentPath: del.DocumentPath,
ShippingCostItem: del.ShippingCostItem,
ShippingCostTotal: del.ShippingCostTotal,
Document: document,
})
}
return TransferDetailDTO{
TransferListDTO: ToTransferListDTO(e),
Details: details,
+11 -1
View File
@@ -1,10 +1,14 @@
package transfers
import (
"context"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
@@ -29,8 +33,14 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
userRepo := rUser.NewUserRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
documentRepo := commonRepo.NewDocumentRepository(db)
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo)
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
if err != nil {
panic(err)
}
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, documentSvc)
userService := sUser.NewUserService(userRepo, validate)
TransferRoutes(router, userService, transferService)
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"mime/multipart"
"strings"
"github.com/go-playground/validator/v10"
@@ -27,7 +28,7 @@ import (
type TransferService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.StockTransfer, error)
CreateOne(ctx *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error)
CreateOne(ctx *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error)
}
type transferService struct {
@@ -42,9 +43,10 @@ type transferService struct {
SupplierRepo rSupplier.SupplierRepository
WarehouseRepo warehouseRepo.WarehouseRepository
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
DocumentSvc commonSvc.DocumentService
}
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository) TransferService {
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService) TransferService {
return &transferService{
Log: utils.Log,
Validate: validate,
@@ -57,6 +59,7 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
SupplierRepo: supplierRepo,
WarehouseRepo: warehouseRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
DocumentSvc: documentSvc,
}
}
@@ -72,7 +75,10 @@ func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
Preload("Details").
Preload("Details.Product").
Preload("Deliveries.Items").
Preload("Deliveries.Supplier")
Preload("Deliveries.Supplier").
Preload("Deliveries.Documents", func(db *gorm.DB) *gorm.DB {
return db.Where("documentable_type = ?", string(utils.DocumentableTypeTransfer))
})
}
func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error) {
@@ -94,31 +100,31 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
return nil, 0, err
}
s.Log.Infof("Retrieved %d transfers", len(transfers))
return transfers, total, nil
}
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
var transfer entity.StockTransfer
s.Log.Infof("Attempting to get StockTransfer with ID: %d", id)
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return s.withRelations(db)
})
if err != nil {
s.Log.Errorf("Error getting StockTransfer ID %d: %+v", id, err)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
}
s.Log.Errorf("Failed to get transfer by ID: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
}
s.Log.Infof("Retrieved transfer: %+v", transfer)
if transferPtr != nil {
s.Log.Infof("StockTransfer %d has %d documents", transferPtr.Id, len(transferPtr.Documents))
}
return transferPtr, nil
}
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest, files []*multipart.FileHeader) (*entity.StockTransfer, error) {
pwIDs := make([]uint, 0, len(req.Products))
@@ -180,7 +186,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
seqNum, err := s.StockTransferRepo.GetNextMovementNumber(c.Context())
if err != nil {
s.Log.Errorf("Failed to get next movement number: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate movement number")
}
movementNumber := fmt.Sprintf("PND-MBU-%05d", seqNum)
@@ -198,10 +203,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
err = s.StockTransferRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
if err := s.StockTransferRepo.WithTx(tx).CreateOne(c.Context(), entityTransfer, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer: %+v", err)
return err
}
s.Log.Infof("Stock transfer created: %+v", entityTransfer.Id)
var details []*entity.StockTransferDetail
for _, product := range req.Products {
@@ -212,10 +215,8 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
})
}
if err := s.StockTransferDetailRepo.WithTx(tx).CreateMany(c.Context(), details, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer details: %+v", err)
return err
}
s.Log.Infof("Stock transfer details created for transfer ID: %+v", entityTransfer.Id)
var deliveries []*entity.StockTransferDelivery
for _, delivery := range req.Deliveries {
@@ -224,13 +225,11 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
SupplierId: uint64(delivery.SupplierID),
VehiclePlate: delivery.VehiclePlate,
DriverName: delivery.DriverName,
DocumentPath: "https://tourism.gov.in/sites/default/files/2019-04/dummy-pdf_2.pdf",
ShippingCostItem: delivery.DeliveryCostPerItem,
ShippingCostTotal: delivery.DeliveryCost,
})
}
if err := s.StockTransferDeliveryRepo.WithTx(tx).CreateMany(c.Context(), deliveries, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer deliveries: %+v", err)
return err
}
@@ -256,38 +255,53 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
}
}
if err := s.StockTransferDeliveryItemRepo.WithTx(tx).CreateMany(c.Context(), deliveryItems, nil); err != nil {
s.Log.Errorf("Failed to create stock transfer delivery items: %+v", err)
return err
}
s.Log.Infof("Stock transfer delivery items created for transfer ID: %+v", entityTransfer.Id)
if s.DocumentSvc != nil && len(files) > 0 {
for idx, file := range files {
documentFiles := []commonSvc.DocumentFile{
{
File: file,
Type: string(utils.DocumentTypeTransfer),
Index: &idx,
},
}
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: string(utils.DocumentableTypeTransfer),
DocumentableID: deliveries[idx].Id,
CreatedBy: &actorID,
Files: documentFiles,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d", idx+1))
}
}
}
for _, product := range req.Products {
sourcePW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), uint(product.ProductID), uint(req.SourceWarehouseID))
if err != nil {
s.Log.Errorf("Failed to get source product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get source product warehouse")
}
if sourcePW.Quantity < product.ProductQty {
s.Log.Errorf("Insufficient stock in source warehouse for product ID: %+v", product.ProductID)
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock in source warehouse for product ID: %d", product.ProductID))
}
sourcePW.Quantity -= product.ProductQty
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), sourcePW.Id, sourcePW, nil); err != nil {
s.Log.Errorf("Failed to update source product warehouse: %+v", err)
return err
}
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
decreaseLog := &entity.StockLog{
Decrease: product.ProductQty,
Notes: "",
LoggableType: entity.LogTypeTransfer,
LoggableType: string(utils.StockLogTypeTransfer),
LoggableId: uint(entityTransfer.Id),
ProductWarehouseId: sourcePW.Id,
CreatedBy: actorID,
}
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), decreaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log decrease: %+v", err)
return err
}
@@ -295,7 +309,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to get destination product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get destination product warehouse")
}
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
@@ -311,29 +324,24 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
ProjectFlockKandangId: &projectFlockKandangID,
}
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
s.Log.Errorf("Failed to create destination product warehouse: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create destination product warehouse")
}
s.Log.Infof("Destination product warehouse created: %+v", destPW.Id)
}
destPW.Quantity += product.ProductQty
if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(c.Context(), destPW.Id, destPW, nil); err != nil {
s.Log.Errorf("Failed to update destination product warehouse: %+v", err)
return err
}
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
increaseLog := &entity.StockLog{
Increase: product.ProductQty,
LoggableType: entity.LogTypeTransfer,
LoggableType: string(utils.StockLogTypeTransfer),
LoggableId: uint(entityTransfer.Id),
Notes: "",
ProductWarehouseId: destPW.Id,
CreatedBy: actorID,
}
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), increaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log increase: %+v", err)
return err
}
}
@@ -343,7 +351,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
if err != nil {
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to process transfer transaction")
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err))
}
result, err := s.GetOne(c, uint(entityTransfer.Id))
@@ -359,7 +367,6 @@ func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, wa
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID))
}
s.Log.Errorf("Failed to get warehouse %d: %+v", warehouseID, err)
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang")
}
@@ -372,7 +379,6 @@ func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, wa
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId))
}
s.Log.Errorf("Failed to get active project flock for kandang %d: %+v", *warehouse.KandangId, err)
return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang")
}
@@ -121,7 +121,7 @@ func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingD
return MarketingDeliveryProductDTO{
Id: e.Id,
MarketingProductId: e.MarketingProductId,
Qty: e.Qty,
Qty: e.UsageQty,
UnitPrice: e.UnitPrice,
TotalWeight: e.TotalWeight,
AvgWeight: e.AvgWeight,
+29 -4
View File
@@ -2,6 +2,7 @@ package marketing
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -13,11 +14,12 @@ import (
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
)
type MarketingModule struct{}
@@ -31,6 +33,28 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
customerRepo := rCustomer.NewCustomerRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
// Initialize FIFO service
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
// Register marketing_delivery_products as FIFO Usable
// Note: ProductWarehouseID comes from marketing_products table via preload
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyMarketingDelivery,
Table: "marketing_delivery_products",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id", // Resolved from marketing_products via preload
UsageQuantity: "usage_qty",
PendingQuantity: "pending_qty",
CreatedAt: "created_at",
},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register marketing delivery usable workflow: %v", err))
}
}
// Initialize approval service
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
@@ -42,9 +66,10 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
// Initialize services
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc,warehouseRepo,projectFlockKandangRepo, validate)
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate)
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, warehouseRepo, projectFlockKandangRepo, validate)
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, fifoService, validate)
userService := sUser.NewUserService(userRepo, validate)
// Register routes
@@ -17,6 +17,9 @@ type MarketingDeliveryProductRepository interface {
GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error)
GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error)
GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.MarketingQuery) ([]entity.MarketingDeliveryProduct, int64, error)
UpdateFifoFields(ctx context.Context, id uint, usageQty, pendingQty float64) error
GetUsageQty(ctx context.Context, id uint) (float64, error)
ResetFifoFields(ctx context.Context, id uint) error
}
type MarketingDeliveryProductRepositoryImpl struct {
@@ -241,3 +244,33 @@ func containsJoin(db *gorm.DB, tableName string) bool {
joinSQL := statement.SQL.String()
return strings.Contains(joinSQL, "JOIN "+tableName)
}
func (r *MarketingDeliveryProductRepositoryImpl) UpdateFifoFields(ctx context.Context, id uint, usageQty, pendingQty float64) error {
return r.DB().WithContext(ctx).
Model(&entity.MarketingDeliveryProduct{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"usage_qty": usageQty,
"pending_qty": pendingQty,
}).Error
}
func (r *MarketingDeliveryProductRepositoryImpl) GetUsageQty(ctx context.Context, id uint) (float64, error) {
var usageQty float64
err := r.DB().WithContext(ctx).
Model(&entity.MarketingDeliveryProduct{}).
Where("id = ?", id).
Select("usage_qty").
Scan(&usageQty).Error
return usageQty, err
}
func (r *MarketingDeliveryProductRepositoryImpl) ResetFifoFields(ctx context.Context, id uint) error {
return r.DB().WithContext(ctx).
Model(&entity.MarketingDeliveryProduct{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"usage_qty": 0,
"pending_qty": 0,
}).Error
}
+9 -6
View File
@@ -16,12 +16,15 @@ func RegisterRoutes(router fiber.Router, userService user.UserService, salesOrde
route := router.Group("/marketing")
route.Use(m.Auth(userService))
route.Get("/",m.RequirePermissions(m.P_DeliveryGetAll), deliveryOrdersCtrl.GetAll)
route.Get("/:id",m.RequirePermissions(m.P_DeliveryGetOne), deliveryOrdersCtrl.GetOne)
route.Delete("/:id",m.RequirePermissions(m.P_SalesOrderDelete), salesOrdersCtrl.DeleteOne)
route.Get("/", m.RequirePermissions(m.P_DeliveryGetAll), deliveryOrdersCtrl.GetAll)
route.Get("/:id", m.RequirePermissions(m.P_DeliveryGetOne), deliveryOrdersCtrl.GetOne)
route.Delete("/:id", m.RequirePermissions(m.P_SalesOrderDelete), salesOrdersCtrl.DeleteOne)
route.Post("/sales-orders",m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne)
route.Patch("/sales-orders/:id",m.RequirePermissions(m.P_SalesOrderUpdateOne), salesOrdersCtrl.UpdateOne)
route.Post("/sales-orders/approvals",m.RequirePermissions(m.P_SalesOrderApproval), salesOrdersCtrl.Approval)
route.Post("/sales-orders", m.RequirePermissions(m.P_SalesOrderCreateOne), salesOrdersCtrl.CreateOne)
route.Patch("/sales-orders/:id", m.RequirePermissions(m.P_SalesOrderUpdateOne), salesOrdersCtrl.UpdateOne)
route.Post("/sales-orders/approvals", m.RequirePermissions(m.P_SalesOrderApproval), salesOrdersCtrl.Approval)
route.Post("/delivery-orders", m.RequirePermissions(m.P_DeliveryCreateOne), deliveryOrdersCtrl.CreateOne)
route.Patch("/delivery-orders/:id", m.RequirePermissions(m.P_DeliveryUpdateOne), deliveryOrdersCtrl.UpdateOne)
}
@@ -15,10 +15,10 @@ import (
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
@@ -30,12 +30,12 @@ type DeliveryOrdersService interface {
}
type deliveryOrdersService struct {
Log *logrus.Logger
Validate *validator.Validate
MarketingRepo marketingRepo.MarketingRepository
MarketingProductRepo marketingRepo.MarketingProductRepository
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
ApprovalSvc commonSvc.ApprovalService
FifoSvc commonSvc.FifoService
}
func NewDeliveryOrdersService(
@@ -43,15 +43,16 @@ func NewDeliveryOrdersService(
marketingProductRepo marketingRepo.MarketingProductRepository,
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
approvalSvc commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService,
validate *validator.Validate,
) DeliveryOrdersService {
return &deliveryOrdersService{
Log: utils.Log,
Validate: validate,
MarketingRepo: marketingRepo,
MarketingProductRepo: marketingProductRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
ApprovalSvc: approvalSvc,
FifoSvc: fifoSvc,
}
}
@@ -108,7 +109,6 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
})
if err != nil {
s.Log.Errorf("Failed to get marketings: %+v", err)
return nil, 0, err
}
for i := range marketings {
@@ -116,7 +116,7 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO
return db.Preload("ActionUser")
})
if err != nil {
s.Log.Warnf("Failed to load approval for marketing %d: %+v", marketings[i].Id, err)
continue
}
marketings[i].LatestApproval = latestApproval
}
@@ -247,7 +247,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
itemDeliveryDate = &parsedDate
}
deliveryProduct.Qty = requestedProduct.Qty
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
deliveryProduct.TotalWeight = requestedProduct.TotalWeight
@@ -256,7 +256,8 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
if requestedProduct.Qty > 0 {
if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, requestedProduct.Qty); err != nil {
if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty); err != nil {
return err
}
}
@@ -354,8 +355,9 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
itemDeliveryDate = deliveryProduct.DeliveryDate
}
oldQty := deliveryProduct.Qty
deliveryProduct.Qty = requestedProduct.Qty
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
deliveryProduct.TotalWeight = requestedProduct.TotalWeight
@@ -363,14 +365,18 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
deliveryProduct.DeliveryDate = itemDeliveryDate
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
qtyChange := requestedProduct.Qty - oldQty
if qtyChange > 0 {
if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, qtyChange); err != nil {
return err
if requestedProduct.Qty != oldRequestedQty {
if oldRequestedQty > 0 {
if err := s.releaseDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct); err != nil {
return err
}
}
} else if qtyChange < 0 {
if err := s.restoreProductWarehouseStock(c.Context(), dbTransaction, foundMarketingProduct, -qtyChange); err != nil {
return err
if requestedProduct.Qty > 0 {
if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty); err != nil {
return err
}
}
}
@@ -393,50 +399,79 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
return s.getMarketingWithDeliveries(c, id)
}
func (s deliveryOrdersService) validateAndReduceProductWarehouse(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyDeliver float64) error {
func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, requestedQty float64) error {
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
}
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
if deliveryProduct == nil || deliveryProduct.Id == 0 {
return fiber.NewError(fiber.StatusInternalServerError, "Delivery product not found")
}
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
ProductWarehouseID: marketingProduct.ProductWarehouseId,
Quantity: requestedQty,
AllowPending: false,
Tx: tx,
})
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found")
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
pw, err2 := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
if err2 != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check product warehouse stock")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock")
if pw == nil || pw.Quantity < requestedQty {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock. Available: %.2f, Requested: %.2f", func() float64 { if pw != nil { return pw.Quantity } else { return 0 } }(), requestedQty))
}
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, requestedQty, 0); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
}
return nil
}
if pw.Quantity < qtyDeliver {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for warehouse - available: %.2f, requested: %.2f", pw.Quantity, qtyDeliver))
if err := deliveryProductRepo.UpdateFifoFields(ctx, deliveryProduct.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
}
pw.Quantity = pw.Quantity - qtyDeliver
if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock")
}
return nil
}
func (s deliveryOrdersService) restoreProductWarehouseStock(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyRestore float64) error {
func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct) error {
if deliveryProduct == nil || deliveryProduct.Id == 0 {
return nil
}
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
}
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx)
pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil)
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
currentUsage, err := deliveryProductRepo.GetUsageQty(ctx, deliveryProduct.Id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock")
currentUsage = 0
}
pw.Quantity = pw.Quantity + qtyRestore
if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock")
if currentUsage == 0 {
return nil
}
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: fifo.UsableKeyMarketingDelivery,
UsableID: deliveryProduct.Id,
Tx: tx,
}); err != nil {
return err
}
if err := deliveryProductRepo.ResetFifoFields(ctx, deliveryProduct.Id); err != nil {
return err
}
return nil
@@ -307,15 +307,17 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
mdp := &entity.MarketingDeliveryProduct{
MarketingProductId: old.Id,
Qty: 0,
UnitPrice: 0,
TotalWeight: 0,
AvgWeight: 0,
TotalPrice: 0,
DeliveryDate: nil,
VehicleNumber: rp.VehicleNumber,
UsageQty: 0,
PendingQty: 0,
}
if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product")
@@ -340,7 +342,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
}
if err == nil {
if deliveryProduct.DeliveryDate != nil || deliveryProduct.Qty > 0 {
if deliveryProduct.DeliveryDate != nil || deliveryProduct.UsageQty > 0 || deliveryProduct.PendingQty > 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id))
}
@@ -602,13 +604,14 @@ func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Cont
marketingDeliveryProduct := &entity.MarketingDeliveryProduct{
MarketingProductId: marketingProduct.Id,
Qty: 0,
UnitPrice: 0,
TotalWeight: 0,
AvgWeight: 0,
TotalPrice: 0,
DeliveryDate: nil,
VehicleNumber: rp.VehicleNumber,
UsageQty: 0,
PendingQty: 0,
}
if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil {
return err
@@ -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"
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses"
productionStandards "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards"
// MODULE IMPORTS
)
@@ -40,6 +41,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
products.ProductModule{},
banks.BankModule{},
flocks.FlockModule{},
productionStandards.ProductionStandardModule{},
// MODULE REGISTRY
}
+31 -2
View File
@@ -2,6 +2,7 @@ package chickins
import (
"fmt"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -9,6 +10,7 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
@@ -36,16 +38,43 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
userRepo := rUser.NewUserRepository(db)
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyProjectChickin,
Table: "project_chickins",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_usage_qty",
CreatedAt: "created_at",
},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register chickin usable workflow: %v", err))
}
}
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register chickin approval workflow: %v", err))
}
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
chickinService := sChickin.NewChickinService(
chickinRepo,
kandangRepo,
warehouseRepo,
productWarehouseRepo,
projectFlockRepo,
projectflockkandangrepo,
projectflockpopulationrepo,
chickinDetailRepo,
validate,
fifoService)
userService := sUser.NewUserService(userRepo, validate)
ChickinRoutes(router, userService, chickinService)
@@ -1,6 +1,7 @@
package service
import (
"context"
"errors"
"fmt"
"strings"
@@ -15,7 +16,9 @@ import (
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
@@ -23,6 +26,8 @@ import (
"gorm.io/gorm"
)
var chickinUsableKey = fifo.UsableKeyProjectChickin
type ChickinService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error)
@@ -43,9 +48,11 @@ type chickinService struct {
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
ProjectChickinDetailRepo repository.ProjectChickinDetailRepository
FifoSvc commonSvc.FifoService
StockLogRepo rStockLogs.StockLogRepository
}
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate) ChickinService {
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoSvc commonSvc.FifoService) ChickinService {
return &chickinService{
Log: utils.Log,
Validate: validate,
@@ -57,6 +64,8 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan
ProjectflockKandangRepo: projectflockkandangRepo,
ProjectflockPopulationRepo: projectflockpopulationRepo,
ProjectChickinDetailRepo: projectChickinDetailRepo,
FifoSvc: fifoSvc,
StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()),
}
}
@@ -124,15 +133,14 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found")
}
category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
newChikins := make([]*entity.ProjectChickin, 0)
chickinQtyMap := make(map[uint]float64)
for _, chickinReq := range req.ChickinRequests {
for idx, chickinReq := range req.ChickinRequests {
productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickinReq.ProductWarehouseId, nil)
if err != nil {
@@ -152,26 +160,23 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId))
}
availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, productWarehouse, category)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to calculate available quantity for product warehouse %d", chickinReq.ProductWarehouseId))
}
availableQty := productWarehouse.Quantity
if availableQty <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No available quantity for product warehouse %d", chickinReq.ProductWarehouseId))
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No available stock in product warehouse %d for chickin", chickinReq.ProductWarehouseId))
}
newChickin := &entity.ProjectChickin{
ProjectFlockKandangId: req.ProjectFlockKandangId,
ChickInDate: chickinDate,
UsageQty: 0,
PendingUsageQty: availableQty,
PendingUsageQty: 0,
ProductWarehouseId: chickinReq.ProductWarehouseId,
Notes: chickinReq.Note,
CreatedBy: actorID,
}
newChikins = append(newChikins, newChickin)
chickinQtyMap[uint(idx)] = availableQty
}
if len(newChikins) == 0 {
@@ -188,30 +193,23 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
}
for idx, chickin := range newChikins {
desiredQty := chickinQtyMap[uint(idx)]
if err := s.ConsumeChickinStocks(c.Context(), dbTransaction, chickin, desiredQty, actorID); err != nil {
return err
}
}
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, projectFlockKandang.Id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval")
}
if category == string(utils.ProjectFlockCategoryLaying) {
for _, chickin := range newChikins {
updates := map[string]any{"qty": gorm.Expr("qty - ?", chickin.PendingUsageQty)}
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickin.ProductWarehouseId))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update product warehouse quantity")
}
}
}
var approvalAction entity.ApprovalAction
if isFirstTime {
approvalAction = entity.ApprovalActionCreated
@@ -301,6 +299,32 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
chickin, err := s.Repository.GetByID(c.Context(), id, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
}
return err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return err
}
if chickin.UsageQty > 0 {
if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil {
return err
}
warehouseDeltas := make(map[uint]float64)
warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty
if err := s.adjustProductWarehouseQuantities(c.Context(), s.Repository.DB(), warehouseDeltas); err != nil {
s.Log.Errorf("Failed to adjust product warehouses for deleted chickin %d: %+v", chickin.Id, err)
return err
}
}
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
@@ -311,54 +335,6 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
return nil
}
func (s chickinService) calculateAvailableQuantity(ctx *fiber.Ctx, projectFlockKandangID uint, productWarehouse *entity.ProductWarehouse, category string) (float64, error) {
availableQty := productWarehouse.Quantity
if category == string(utils.ProjectFlockCategoryGrowing) {
var totalPendingQty float64
chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
if err == nil {
for _, chickin := range chickins {
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
totalPendingQty += chickin.PendingUsageQty
}
}
}
availableQty = productWarehouse.Quantity - totalPendingQty
if availableQty < 0 {
availableQty = 0
}
} else if category == string(utils.ProjectFlockCategoryLaying) {
var totalPopulation float64
var totalPendingQty float64
populations, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(ctx.Context(), projectFlockKandangID, productWarehouse.Id)
if err == nil {
for _, pop := range populations {
totalPopulation += pop.TotalQty
}
}
chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID)
if err == nil {
for _, chickin := range chickins {
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
totalPendingQty += chickin.PendingUsageQty
}
}
}
availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty
if availableQty < 0 {
availableQty = 0
}
}
return availableQty, nil
}
func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
@@ -387,11 +363,10 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
}
for _, id := range approvableIDs {
idCopy := id
if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &idCopy, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil {
if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &id, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil {
return nil, err
}
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, id, nil)
if err != nil {
@@ -414,7 +389,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
for _, approvableID := range approvableIDs {
if _, err := approvalSvc.CreateApproval(
@@ -472,9 +446,9 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
}
pfkID := approvableID
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID, &pfkID)
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "LAYER", dbTransaction, actorID, &pfkID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create LAYER product warehouse")
}
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
@@ -491,27 +465,17 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
continue
}
kandangForRejection, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID))
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang")
}
categoryForRejection := strings.ToUpper(strings.TrimSpace(kandangForRejection.ProjectFlock.Category))
for _, chickin := range chickins {
if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) {
updates := map[string]any{"qty": gorm.Expr("qty + ?", chickin.PendingUsageQty)}
if err := s.ReleaseChickinStocks(c.Context(), dbTransaction, &chickin, actorID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for rejected chickin %d: %v", chickin.Id, err))
}
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId))
}
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to restore product warehouse quantity for chickin %d", chickin.Id))
}
warehouseDeltas := make(map[uint]float64)
warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty
if err := s.adjustProductWarehouseQuantities(c.Context(), dbTransaction, warehouseDeltas); err != nil {
s.Log.Errorf("Failed to adjust product warehouses for rejected chickin %d: %+v", chickin.Id, err)
return err
}
if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil {
@@ -549,7 +513,7 @@ func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId
products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
if err == nil && len(products) > 0 {
existingPW := &products[0]
// Update project_flock_kandang_id if not already set
if existingPW.ProjectFlockKandangId == nil && projectFlockKandangId != nil {
existingPW.ProjectFlockKandangId = projectFlockKandangId
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).UpdateOne(ctx.Context(), existingPW.Id, existingPW, nil); err != nil {
@@ -572,7 +536,6 @@ func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId
WarehouseId: warehouseId,
ProjectFlockKandangId: projectFlockKandangId,
Quantity: 0,
// CreatedBy: actorID,
}
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil {
@@ -588,10 +551,10 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
return fmt.Errorf("invalid target product warehouse")
}
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
var totalQuantityAdded float64
for _, chickin := range chickins {
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id)
@@ -604,34 +567,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
continue
}
quantityToConvert := chickin.PendingUsageQty
if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{
"usage_qty": quantityToConvert,
"pending_usage_qty": 0,
}, nil); err != nil {
return fmt.Errorf("failed to update chickin %d qty: %w", chickin.Id, err)
}
if chickin.ProductWarehouseId != targetPW.Id {
if err := productWarehouseTx.PatchOne(ctx.Context(), chickin.ProductWarehouseId, map[string]any{
"qty": gorm.Expr("qty - ?", quantityToConvert),
}, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Source product warehouse %d not found", chickin.ProductWarehouseId))
}
return fmt.Errorf("failed to deduct source warehouse quantity for chickin %d: %w", chickin.Id, err)
}
}
if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{
"qty": gorm.Expr("qty + ?", quantityToConvert),
}, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id))
}
return fmt.Errorf("failed to update target warehouse quantity: %w", err)
}
quantityToConvert := chickin.UsageQty
population := &entity.ProjectFlockPopulation{
ProjectChickinId: chickin.Id,
@@ -644,7 +580,121 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil {
return err
}
totalQuantityAdded += quantityToConvert
}
if totalQuantityAdded > 0 {
if err := s.ProductWarehouseRepo.AdjustQuantities(ctx.Context(), map[uint]float64{
targetPW.Id: totalQuantityAdded,
}, func(db *gorm.DB) *gorm.DB {
return dbTransaction
}); err != nil {
return fmt.Errorf("failed to update target product warehouse quantity: %w", err)
}
}
return nil
}
func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, desiredQty float64, actorID uint) error {
if chickin == nil || s.FifoSvc == nil {
return nil
}
s.Log.Infof("ConsumeChickinStocks: chickin_id=%d, product_warehouse_id=%d, desired_qty=%.3f",
chickin.Id, chickin.ProductWarehouseId, desiredQty)
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: chickinUsableKey,
UsableID: chickin.Id,
ProductWarehouseID: chickin.ProductWarehouseId,
Quantity: desiredQty,
AllowPending: true,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to consume FIFO stock for chickin %d: %+v", chickin.Id, err)
return err
}
s.Log.Infof("ConsumeChickinStocks result: usage_qty=%.3f, pending_qty=%.3f, allocated_allocations=%d",
result.UsageQuantity, result.PendingQuantity, len(result.AddedAllocations))
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Updates(map[string]interface{}{
"usage_qty": result.UsageQuantity,
"pending_usage_qty": result.PendingQuantity,
}).Error; err != nil {
return err
}
if result.UsageQuantity > 0 {
decreaseLog := &entity.StockLog{
Decrease: result.UsageQuantity,
LoggableType: string(utils.StockLogTypeChikin),
LoggableId: chickin.Id,
ProductWarehouseId: chickin.ProductWarehouseId,
CreatedBy: actorID,
Notes: fmt.Sprintf("Chickin #%d", chickin.Id),
}
if err := s.StockLogRepo.CreateOne(ctx, decreaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log for chickin %d: %+v", chickin.Id, err)
}
}
return nil
}
func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, actorID uint) error {
if chickin == nil || s.FifoSvc == nil {
return nil
}
var currentUsage float64
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(&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
if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryGrowing) {
var totalPendingQty float64
for _, chickin := range projectFlockKandang.Chickins {
@@ -568,15 +569,8 @@ func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehous
availableQty = 0
}
} else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) {
var totalPopulation float64
var totalPendingQty float64
populations, err := s.PopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(c.Context(), projectFlockKandang.Id, productWarehouse.Id)
if err == nil {
for _, pop := range populations {
totalPopulation += pop.TotalQty
}
}
var totalPendingQty float64
for _, chickin := range projectFlockKandang.Chickins {
if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 {
@@ -584,7 +578,7 @@ func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehous
}
}
availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty
availableQty = productWarehouse.Quantity - totalPendingQty
if availableQty < 0 {
availableQty = 0
}
+7
View File
@@ -1,6 +1,7 @@
package purchases
import (
"context"
"fmt"
"github.com/go-playground/validator/v10"
@@ -41,6 +42,11 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
documentRepo := commonRepo.NewDocumentRepository(db)
documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo)
if err != nil {
panic(fmt.Sprintf("failed to create document service: %v", err))
}
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPurchase, utils.PurchaseApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register purchase approval workflow: %v", err))
}
@@ -54,6 +60,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
approvalService,
expenseRealizationRepo,
projectFlockKandangRepository,
documentSvc,
validate,
)
expenseBridge := service.NewExpenseBridge(
@@ -61,7 +61,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
doNumber := marketingDTO.GenerateDeliveryOrderNumber(mdp.MarketingProduct.Marketing.SoNumber, mdp.DeliveryDate, mdp.MarketingProduct.ProductWarehouse.WarehouseId)
totalWeightKg := mdp.Qty * mdp.AvgWeight
totalWeightKg := mdp.UsageQty * mdp.AvgWeight
salesAmount := totalWeightKg * mdp.UnitPrice
var hpp float64
@@ -78,7 +78,7 @@ func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerK
AgingDays: agingDays,
DoNumber: doNumber,
MarketingType: getMarketingType(mdp),
Qty: mdp.Qty,
Qty: mdp.UsageQty,
AverageWeightKg: mdp.AvgWeight,
TotalWeightKg: totalWeightKg,
SalesPricePerKg: mdp.UnitPrice,
@@ -194,8 +194,8 @@ func ToSummary(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64, ca
totalHppAmount := int64(0)
for _, mdp := range mdps {
calculatedTotalWeight := mdp.Qty * mdp.AvgWeight
totalQty += int(mdp.Qty)
calculatedTotalWeight := mdp.UsageQty * mdp.AvgWeight
totalQty += int(mdp.UsageQty)
totalWeightKg += calculatedTotalWeight
totalSalesAmount += int64(calculatedTotalWeight * mdp.UnitPrice)
@@ -138,7 +138,6 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing
func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, category string) float64 {
totalCost := s.getTotalProjectCost(ctx, projectFlockID)
if totalCost == 0 {
s.Log.Warnf("HPP calculation: No cost found for project flock ID %d. Check if purchase items are linked to project_flock_kandang_id", projectFlockID)
return 0
}
+3 -2
View File
@@ -17,9 +17,9 @@ import (
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases"
repports "gitlab.com/mbugroup/lti-api.git/internal/modules/repports"
ssoModule "gitlab.com/mbugroup/lti-api.git/internal/modules/sso"
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
repports "gitlab.com/mbugroup/lti-api.git/internal/modules/repports"
// MODULE IMPORTS
)
@@ -43,7 +43,8 @@ func Routes(app *fiber.App, db *gorm.DB) {
expenses.ExpenseModule{},
ssoModule.Module{},
closings.ClosingModule{},
repports.RepportModule{},
repports.RepportModule{},
// MODULE REGISTRY
}
+20 -1
View File
@@ -111,6 +111,8 @@ type StockLogType string
const (
StockLogTypeAdjustment StockLogType = "ADJUSTMENT"
StockLogTypeTransfer StockLogType = "TRANSFER"
StockLogTypeMarketing StockLogType = "MARKETING"
StockLogTypeChikin StockLogType = "CHICKIN"
)
// -------------------------------------------------------------------
@@ -314,6 +316,23 @@ var ExpenseApprovalSteps = map[approvalutils.ApprovalStep]string{
ExpenseStepSelesai: "Selesai",
}
// -------------------------------------------------------------------
// Document
// -------------------------------------------------------------------
type DocumentType string
type DocumentableType string
const (
DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT"
DocumentTypeExpense DocumentType = "EXPENSE_DOCUMENT"
DocumentTypeExpenseRealization DocumentType = "EXPENSE_REALIZATION_DOCUMENT"
DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER"
DocumentableTypeExpense DocumentableType = "EXPENSE"
DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION"
)
// -------------------------------------------------------------------
// Validators
// -------------------------------------------------------------------
@@ -448,7 +467,7 @@ func IsValidExpenseCategory(v string) bool {
return false
}
// example use
// e xample use
// Recording helper
+3 -1
View File
@@ -1,5 +1,7 @@
package fifo
const (
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
UsableKeyRecordingStock UsableKey = "RECORDING_STOCK"
UsableKeyProjectChickin UsableKey = "PROJECT_CHICKIN"
UsableKeyMarketingDelivery UsableKey = "MARKETING_DELIVERY"
)