mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
unfinished purchase
This commit is contained in:
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS purchase_items;
|
||||
@@ -0,0 +1,59 @@
|
||||
CREATE TABLE IF NOT EXISTS purchase_items (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
purchase_id BIGINT NOT NULL,
|
||||
product_id BIGINT NOT NULL,
|
||||
warehouse_id BIGINT NOT NULL,
|
||||
product_warehouse_id BIGINT,
|
||||
received_date TIMESTAMPTZ,
|
||||
travel_number VARCHAR,
|
||||
travel_number_docs VARCHAR,
|
||||
vehicle_number VARCHAR,
|
||||
sub_qty NUMERIC(15, 3) NOT NULL,
|
||||
total_qty NUMERIC(15, 3) DEFAULT 0,
|
||||
total_used NUMERIC(15, 3) DEFAULT 0,
|
||||
price NUMERIC(15, 3) DEFAULT 0,
|
||||
total_price NUMERIC(15, 3) DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'products') THEN
|
||||
EXECUTE
|
||||
'ALTER TABLE purchase_items
|
||||
ADD CONSTRAINT fk_purchase_items_product
|
||||
FOREIGN KEY (product_id)
|
||||
REFERENCES products(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'warehouses') THEN
|
||||
EXECUTE
|
||||
'ALTER TABLE purchase_items
|
||||
ADD CONSTRAINT fk_purchase_items_warehouse
|
||||
FOREIGN KEY (warehouse_id)
|
||||
REFERENCES warehouses(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
|
||||
EXECUTE
|
||||
'ALTER TABLE purchase_items
|
||||
ADD CONSTRAINT fk_purchase_items_product_warehouse
|
||||
FOREIGN KEY (product_warehouse_id)
|
||||
REFERENCES product_warehouses(id)
|
||||
ON DELETE SET NULL ON UPDATE CASCADE';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_purchase_items_unique_allocation
|
||||
ON purchase_items (purchase_id, product_id, warehouse_id)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_id ON purchase_items (product_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_purchase_items_warehouse_id ON purchase_items (warehouse_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_purchase_items_product_warehouse_id ON purchase_items (product_warehouse_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_purchase_items_purchase_id ON purchase_items (purchase_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_purchase_items_deleted_at ON purchase_items (deleted_at);
|
||||
@@ -0,0 +1,14 @@
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'fk_purchase_items_purchase'
|
||||
AND conrelid = 'purchase_items'::regclass
|
||||
) THEN
|
||||
ALTER TABLE purchase_items
|
||||
DROP CONSTRAINT fk_purchase_items_purchase;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DROP TABLE IF EXISTS purchases;
|
||||
@@ -0,0 +1,64 @@
|
||||
CREATE TABLE IF NOT EXISTS purchases (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
pr_number VARCHAR NOT NULL,
|
||||
po_number VARCHAR,
|
||||
po_date TIMESTAMPTZ,
|
||||
supplier_id BIGINT NOT NULL,
|
||||
credit_term INT,
|
||||
due_date TIMESTAMPTZ,
|
||||
grand_total NUMERIC(15, 3) DEFAULT 0,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by BIGINT NOT NULL
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN
|
||||
EXECUTE
|
||||
'ALTER TABLE purchases
|
||||
ADD CONSTRAINT fk_purchases_supplier
|
||||
FOREIGN KEY (supplier_id)
|
||||
REFERENCES suppliers(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
|
||||
EXECUTE
|
||||
'ALTER TABLE purchases
|
||||
ADD CONSTRAINT fk_purchases_created_by
|
||||
FOREIGN KEY (created_by)
|
||||
REFERENCES users(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_tables WHERE tablename = 'purchase_items'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'fk_purchase_items_purchase'
|
||||
) THEN
|
||||
EXECUTE
|
||||
'ALTER TABLE purchase_items
|
||||
ADD CONSTRAINT fk_purchase_items_purchase
|
||||
FOREIGN KEY (purchase_id)
|
||||
REFERENCES purchases(id)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_purchases_pr_number_unique
|
||||
ON purchases (pr_number)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_purchases_po_number_unique
|
||||
ON purchases (po_number)
|
||||
WHERE deleted_at IS NULL AND po_number IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_purchases_supplier_id ON purchases (supplier_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_purchases_created_by ON purchases (created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_purchases_po_date ON purchases (po_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_purchases_deleted_at ON purchases (deleted_at);
|
||||
@@ -0,0 +1,26 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Purchase struct {
|
||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||
PrNumber string `gorm:"not null"`
|
||||
PoNumber *string
|
||||
PoDate *time.Time
|
||||
SupplierId uint64 `gorm:"not null"`
|
||||
CreditTerm *int
|
||||
DueDate *time.Time
|
||||
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
Notes *string
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt *time.Time `gorm:"index"`
|
||||
CreatedBy uint64 `gorm:"not null"`
|
||||
|
||||
// Relations
|
||||
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||
Items []PurchaseItem `gorm:"foreignKey:PurchaseId"`
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type PurchaseItem struct {
|
||||
Id uint64 `gorm:"primaryKey;autoIncrement"`
|
||||
PurchaseId uint64 `gorm:"not null"`
|
||||
ProductId uint64 `gorm:"not null"`
|
||||
WarehouseId uint64 `gorm:"not null"`
|
||||
ProductWarehouseId *uint64
|
||||
ReceivedDate *time.Time
|
||||
TravelNumber *string
|
||||
TravelNumberDocs *string
|
||||
VehicleNumber *string
|
||||
SubQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||
TotalQty float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
TotalUsed float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
Price float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
TotalPrice float64 `gorm:"type:numeric(15,3);default:0"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt *time.Time `gorm:"index"`
|
||||
|
||||
// Relations
|
||||
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
||||
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
)
|
||||
|
||||
type ApprovalBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
StepNumber uint16 `json:"step_number"`
|
||||
StepName string `json:"step_name"`
|
||||
Action *string `json:"action"`
|
||||
@@ -27,6 +28,7 @@ type ApprovalGroupDTO struct {
|
||||
|
||||
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
|
||||
dto := ApprovalBaseDTO{
|
||||
Id: e.Id,
|
||||
Notes: e.Notes,
|
||||
}
|
||||
|
||||
|
||||
+15
@@ -23,6 +23,7 @@ type ProductWarehouseRepository interface {
|
||||
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
|
||||
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
|
||||
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
|
||||
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
|
||||
}
|
||||
|
||||
type ProductWarehouseRepositoryImpl struct {
|
||||
@@ -149,6 +150,20 @@ func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, d
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) {
|
||||
var productWarehouse entity.ProductWarehouse
|
||||
err := r.DB().WithContext(ctx).
|
||||
Preload("Product").
|
||||
Preload("Warehouse").
|
||||
Preload("Warehouse.Area").
|
||||
Preload("Warehouse.Location").
|
||||
First(&productWarehouse, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &productWarehouse, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) {
|
||||
var product entity.Product
|
||||
err := r.DB().WithContext(ctx).
|
||||
|
||||
@@ -17,6 +17,7 @@ type WarehouseRepository interface {
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
|
||||
GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
|
||||
GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error)
|
||||
}
|
||||
|
||||
type WarehouseRepositoryImpl struct {
|
||||
@@ -62,6 +63,18 @@ func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId
|
||||
return &warehouse, nil
|
||||
}
|
||||
|
||||
func (r *WarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error) {
|
||||
var warehouse entity.Warehouse
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Area").
|
||||
Preload("Location").
|
||||
First(&warehouse, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &warehouse, nil
|
||||
}
|
||||
|
||||
func (r *WarehouseRepositoryImpl) GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) {
|
||||
var warehouse entity.Warehouse
|
||||
err := r.db.WithContext(ctx).
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.ProjectflockService) {
|
||||
ctrl := controller.NewProjectflockController(s)
|
||||
|
||||
route := v1.Group("/project_flocks")
|
||||
route := v1.Group("/project-flocks")
|
||||
|
||||
// route.Get("/", m.Auth(u), ctrl.GetAll)
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
@@ -27,6 +27,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
|
||||
route.Delete("/:id", ctrl.DeleteOne)
|
||||
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
||||
route.Post("/approvals", ctrl.Approval)
|
||||
route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary)
|
||||
route.Get("/kandangs/:project-flock_kandang-id/periods", ctrl.GetFlockPeriodSummary)
|
||||
|
||||
}
|
||||
|
||||
@@ -254,18 +254,22 @@ func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFloc
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) {
|
||||
var population entity.ProjectFlockPopulation
|
||||
var total float64
|
||||
err := tx.
|
||||
Where("project_flock_kandang_id = ?", projectFlockKandangId).
|
||||
Order("created_at DESC").
|
||||
First(&population).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, nil
|
||||
}
|
||||
Table("project_flock_populations").
|
||||
Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0) AS total_qty").
|
||||
Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
|
||||
Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangId).
|
||||
Scan(&total).Error
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int64(math.Round(population.TotalQty)), nil
|
||||
|
||||
if total < 0 {
|
||||
total = 0
|
||||
}
|
||||
|
||||
return int64(math.Round(total)), nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||
|
||||
@@ -261,6 +261,10 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.BodyWeights == nil && req.Stocks == nil && req.Depletions == nil && req.Eggs == nil {
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
var recordingEntity *entity.Recording
|
||||
@@ -277,12 +281,21 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
}
|
||||
recordingEntity = recording
|
||||
|
||||
hasBodyChanges := req.BodyWeights != nil
|
||||
hasStockChanges := req.Stocks != nil
|
||||
hasDepletionChanges := req.Depletions != nil
|
||||
hasEggChanges := req.Eggs != nil
|
||||
|
||||
if !hasBodyChanges && !hasStockChanges && !hasDepletionChanges && !hasEggChanges {
|
||||
return nil
|
||||
}
|
||||
|
||||
var category string
|
||||
if recordingEntity.ProjectFlockKandang != nil && recordingEntity.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
||||
category = strings.ToUpper(recordingEntity.ProjectFlockKandang.ProjectFlock.Category)
|
||||
}
|
||||
isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying))
|
||||
if req.Eggs != nil {
|
||||
if hasEggChanges {
|
||||
if !isLaying && len(req.Eggs) > 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks")
|
||||
}
|
||||
@@ -291,7 +304,29 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
}
|
||||
}
|
||||
|
||||
if req.BodyWeights != nil {
|
||||
if hasStockChanges {
|
||||
if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if hasDepletionChanges || hasEggChanges {
|
||||
if err := s.ensureProductWarehousesExist(c, nil, req.Depletions, req.Eggs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
hasExistingGradings := false
|
||||
for _, egg := range recordingEntity.Eggs {
|
||||
if len(egg.GradingEggs) > 0 {
|
||||
hasExistingGradings = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
hasEggsAfterUpdate := len(recordingEntity.Eggs) > 0
|
||||
|
||||
if hasBodyChanges {
|
||||
if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil {
|
||||
s.Log.Errorf("Failed to clear body weights: %+v", err)
|
||||
return err
|
||||
@@ -302,11 +337,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
}
|
||||
}
|
||||
|
||||
if req.Stocks != nil {
|
||||
if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if hasStockChanges {
|
||||
existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to list existing stocks: %+v", err)
|
||||
@@ -330,17 +361,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
}
|
||||
}
|
||||
|
||||
if req.Eggs != nil && req.Depletions == nil {
|
||||
if err := s.ensureProductWarehousesExist(c, nil, nil, req.Eggs); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if req.Depletions != nil {
|
||||
if err := s.ensureProductWarehousesExist(c, nil, req.Depletions, req.Eggs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if hasDepletionChanges {
|
||||
existingDepletions, err := s.Repository.ListDepletions(tx, recordingEntity.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to list existing depletions: %+v", err)
|
||||
@@ -364,7 +385,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
}
|
||||
}
|
||||
|
||||
if req.Eggs != nil {
|
||||
if hasEggChanges {
|
||||
existingEggs, err := s.Repository.ListEggs(tx, recordingEntity.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to list existing eggs: %+v", err)
|
||||
@@ -386,18 +407,72 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
||||
s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
hasExistingGradings = false
|
||||
hasEggsAfterUpdate = len(req.Eggs) > 0
|
||||
}
|
||||
|
||||
if hasBodyChanges || hasStockChanges || hasDepletionChanges {
|
||||
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
|
||||
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
action := entity.ApprovalActionUpdated
|
||||
if err := s.createRecordingApproval(ctx, tx, recordingEntity.Id, utils.RecordingStepPengajuan, action, recordingEntity.CreatedBy, nil); err != nil {
|
||||
actorID := recordingEntity.CreatedBy
|
||||
if actorID == 0 {
|
||||
actorID = 1
|
||||
}
|
||||
|
||||
var step approvalutils.ApprovalStep
|
||||
if isLaying {
|
||||
if !hasEggsAfterUpdate {
|
||||
step = utils.RecordingStepGradingTelur
|
||||
} else if hasEggChanges {
|
||||
step = utils.RecordingStepGradingTelur
|
||||
} else if hasExistingGradings {
|
||||
step = utils.RecordingStepPengajuan
|
||||
} else {
|
||||
step = utils.RecordingStepGradingTelur
|
||||
}
|
||||
} else {
|
||||
step = utils.RecordingStepPengajuan
|
||||
}
|
||||
|
||||
latestApproval := recordingEntity.LatestApproval
|
||||
if latestApproval == nil {
|
||||
if s.ApprovalSvc != nil {
|
||||
if fetched, fetchErr := s.ApprovalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowRecording, recordingEntity.Id, nil); fetchErr != nil {
|
||||
s.Log.Errorf("Failed to load latest approval for recording %d: %+v", recordingEntity.Id, fetchErr)
|
||||
return fetchErr
|
||||
} else {
|
||||
latestApproval = fetched
|
||||
}
|
||||
} else if s.ApprovalRepo != nil {
|
||||
if fetched, fetchErr := s.ApprovalRepo.LatestByTarget(ctx, utils.ApprovalWorkflowRecording.String(), recordingEntity.Id, nil); fetchErr != nil {
|
||||
s.Log.Errorf("Failed to load latest approval for recording %d: %+v", recordingEntity.Id, fetchErr)
|
||||
return fetchErr
|
||||
} else {
|
||||
latestApproval = fetched
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shouldCreateApproval := true
|
||||
if latestApproval != nil &&
|
||||
latestApproval.StepNumber == uint16(step) &&
|
||||
latestApproval.Action != nil &&
|
||||
*latestApproval.Action == action {
|
||||
shouldCreateApproval = false
|
||||
}
|
||||
|
||||
if shouldCreateApproval {
|
||||
if err := s.createRecordingApproval(ctx, tx, recordingEntity.Id, step, action, actorID, nil); err != nil {
|
||||
s.Log.Errorf("Failed to create approval after recording update %d: %+v", recordingEntity.Id, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -1015,13 +1090,21 @@ func (s *recordingService) ensureChickInExists(ctx context.Context, projectFlock
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
|
||||
}
|
||||
|
||||
_, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock belum melakukan chick in sehingga belum dapat membuat recording")
|
||||
}
|
||||
populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to check project flock population for project_flock_kandang_id=%d: %+v", projectFlockKandangID, err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memeriksa data chick in")
|
||||
}
|
||||
|
||||
if len(populations) == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Project flock belum memiliki chick in yang disetujui sehingga belum dapat membuat recording")
|
||||
}
|
||||
|
||||
for _, population := range populations {
|
||||
if population.TotalQty > 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording")
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ type (
|
||||
BodyWeight struct {
|
||||
AvgWeight float64 `json:"avg_weight" validate:"required"`
|
||||
Qty float64 `json:"qty" validate:"required,gt=0"`
|
||||
TotalWeight float64 `json:"total_weight" validate:"required,gte=0"`
|
||||
TotalWeight *float64 `json:"total_weight,omitempty" validate:"omitempty,gte=0"`
|
||||
}
|
||||
|
||||
Stock struct {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type PurchaseController struct {
|
||||
service service.PurchaseService
|
||||
}
|
||||
|
||||
func NewPurchaseController(s service.PurchaseService) *PurchaseController {
|
||||
return &PurchaseController{service: s}
|
||||
}
|
||||
|
||||
func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.CreatePurchaseRequest)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := ctrl.service.CreateOne(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusCreated,
|
||||
Status: "success",
|
||||
Message: "Purchase requisition created successfully",
|
||||
Data: dto.ToPurchaseDetailDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
id, err := strconv.ParseUint(param, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase requisition id")
|
||||
}
|
||||
|
||||
req := new(validation.ApproveStaffPurchaseRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := ctrl.service.ApproveStaffPurchase(c, id, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Staff purchase approval recorded successfully",
|
||||
Data: dto.ToPurchaseDetailDTO(*result),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
)
|
||||
|
||||
type SupplierBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Alias string `json:"alias"`
|
||||
Type string `json:"type"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
type AreaBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type LocationBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type WarehouseBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Area *AreaBaseDTO `json:"area,omitempty"`
|
||||
Location *LocationBaseDTO `json:"location,omitempty"`
|
||||
}
|
||||
|
||||
type ProductBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
SKU *string `json:"sku,omitempty"`
|
||||
}
|
||||
|
||||
type PurchaseItemDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
Product *ProductBaseDTO `json:"product,omitempty"`
|
||||
Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"`
|
||||
ProductWarehouseID *uint64 `json:"product_warehouse_id,omitempty"`
|
||||
SubQty float64 `json:"sub_qty"`
|
||||
TotalQty float64 `json:"total_qty"`
|
||||
TotalUsed float64 `json:"total_used"`
|
||||
Price float64 `json:"price"`
|
||||
TotalPrice float64 `json:"total_price"`
|
||||
}
|
||||
|
||||
type PurchaseDetailDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
PrNumber string `json:"pr_number"`
|
||||
Supplier *SupplierBaseDTO `json:"supplier,omitempty"`
|
||||
CreditTerm *int `json:"credit_term,omitempty"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
GrandTotal float64 `json:"grand_total"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
Items []PurchaseItemDTO `json:"items"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func toSupplierBaseDTO(s entity.Supplier) *SupplierBaseDTO {
|
||||
if s.Id == 0 {
|
||||
return nil
|
||||
}
|
||||
return &SupplierBaseDTO{
|
||||
Id: s.Id,
|
||||
Name: s.Name,
|
||||
Alias: s.Alias,
|
||||
Type: s.Type,
|
||||
Category: s.Category,
|
||||
}
|
||||
}
|
||||
|
||||
func toWarehouseBaseDTO(w *entity.Warehouse) *WarehouseBaseDTO {
|
||||
if w == nil || w.Id == 0 {
|
||||
return nil
|
||||
}
|
||||
dto := &WarehouseBaseDTO{
|
||||
Id: w.Id,
|
||||
Name: w.Name,
|
||||
}
|
||||
if w.Area.Id != 0 {
|
||||
dto.Area = &AreaBaseDTO{
|
||||
Id: w.Area.Id,
|
||||
Name: w.Area.Name,
|
||||
}
|
||||
}
|
||||
if w.Location != nil && w.Location.Id != 0 {
|
||||
dto.Location = &LocationBaseDTO{
|
||||
Id: w.Location.Id,
|
||||
Name: w.Location.Name,
|
||||
}
|
||||
}
|
||||
return dto
|
||||
}
|
||||
|
||||
func toProductBaseDTO(p *entity.Product) *ProductBaseDTO {
|
||||
if p == nil || p.Id == 0 {
|
||||
return nil
|
||||
}
|
||||
dto := &ProductBaseDTO{
|
||||
Id: p.Id,
|
||||
Name: p.Name,
|
||||
}
|
||||
if p.Sku != nil {
|
||||
dto.SKU = p.Sku
|
||||
}
|
||||
return dto
|
||||
}
|
||||
|
||||
func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
|
||||
dto := PurchaseItemDTO{
|
||||
Id: item.Id,
|
||||
ProductWarehouseID: item.ProductWarehouseId,
|
||||
SubQty: item.SubQty,
|
||||
TotalQty: item.TotalQty,
|
||||
TotalUsed: item.TotalUsed,
|
||||
Price: item.Price,
|
||||
TotalPrice: item.TotalPrice,
|
||||
}
|
||||
if item.Product != nil {
|
||||
dto.Product = toProductBaseDTO(item.Product)
|
||||
}
|
||||
if item.Warehouse != nil {
|
||||
dto.Warehouse = toWarehouseBaseDTO(item.Warehouse)
|
||||
}
|
||||
return dto
|
||||
}
|
||||
|
||||
func ToPurchaseItemDTOs(items []entity.PurchaseItem) []PurchaseItemDTO {
|
||||
result := make([]PurchaseItemDTO, len(items))
|
||||
for i, item := range items {
|
||||
result[i] = ToPurchaseItemDTO(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToPurchaseDetailDTO(p entity.Purchase) PurchaseDetailDTO {
|
||||
return PurchaseDetailDTO{
|
||||
Id: p.Id,
|
||||
PrNumber: p.PrNumber,
|
||||
Supplier: toSupplierBaseDTO(p.Supplier),
|
||||
CreditTerm: p.CreditTerm,
|
||||
DueDate: p.DueDate,
|
||||
GrandTotal: p.GrandTotal,
|
||||
Notes: p.Notes,
|
||||
Items: ToPurchaseItemDTOs(p.Items),
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package purchases
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PurchaseModule struct{}
|
||||
|
||||
func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
RegisterRoutes(router, db, validate)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PurchaseRepository interface {
|
||||
repository.BaseRepository[entity.Purchase]
|
||||
CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error
|
||||
GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error)
|
||||
UpdatePricing(ctx context.Context, purchaseID uint64, updates []PurchasePricingUpdate, grandTotal float64) error
|
||||
}
|
||||
|
||||
type PurchaseRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.Purchase]
|
||||
}
|
||||
|
||||
func NewPurchaseRepository(db *gorm.DB) PurchaseRepository {
|
||||
return &PurchaseRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Purchase](db),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error {
|
||||
db := r.DB().WithContext(ctx)
|
||||
|
||||
if err := db.Create(purchase).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
item.PurchaseId = purchase.Id
|
||||
}
|
||||
|
||||
if err := db.Create(&items).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) {
|
||||
var purchase entity.Purchase
|
||||
err := r.DB().WithContext(ctx).
|
||||
Preload("Supplier").
|
||||
Preload("Items", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("id ASC")
|
||||
}).
|
||||
Preload("Items.Product").
|
||||
Preload("Items.Warehouse").
|
||||
Preload("Items.Warehouse.Area").
|
||||
Preload("Items.Warehouse.Location").
|
||||
Preload("Items.ProductWarehouse").
|
||||
First(&purchase, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &purchase, nil
|
||||
}
|
||||
|
||||
type PurchasePricingUpdate struct {
|
||||
ItemID uint64
|
||||
Price float64
|
||||
TotalPrice float64
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) UpdatePricing(
|
||||
ctx context.Context,
|
||||
purchaseID uint64,
|
||||
updates []PurchasePricingUpdate,
|
||||
grandTotal float64,
|
||||
) error {
|
||||
if len(updates) == 0 {
|
||||
return errors.New("pricing updates cannot be empty")
|
||||
}
|
||||
|
||||
db := r.DB().WithContext(ctx)
|
||||
|
||||
for _, upd := range updates {
|
||||
result := db.Model(&entity.PurchaseItem{}).
|
||||
Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
|
||||
Updates(map[string]interface{}{
|
||||
"price": upd.Price,
|
||||
"total_price": upd.TotalPrice,
|
||||
"updated_at": gorm.Expr("NOW()"),
|
||||
})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.Model(&entity.Purchase{}).
|
||||
Where("id = ?", purchaseID).
|
||||
Updates(map[string]interface{}{
|
||||
"grand_total": grandTotal,
|
||||
"updated_at": gorm.Expr("NOW()"),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package purchases
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
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"
|
||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/controllers"
|
||||
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
group := router.Group("/purchases")
|
||||
|
||||
purchaseRepo := rPurchase.NewPurchaseRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
supplierRepo := rSupplier.NewSupplierRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPurchase, utils.PurchaseApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register purchase approval workflow: %v", err))
|
||||
}
|
||||
|
||||
purchaseService := service.NewPurchaseService(
|
||||
validate,
|
||||
purchaseRepo,
|
||||
productWarehouseRepo,
|
||||
warehouseRepo,
|
||||
supplierRepo,
|
||||
approvalRepo,
|
||||
)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
PurchaseRoutes(group, userService, purchaseService)
|
||||
}
|
||||
|
||||
func PurchaseRoutes(v1 fiber.Router, u sUser.UserService, s service.PurchaseService) {
|
||||
ctrl := controller.NewPurchaseController(s)
|
||||
|
||||
route := v1.Group("/requisitions")
|
||||
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
route.Post("/:id/approvals/staff", ctrl.ApproveStaffPurchase)
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
|
||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PurchaseService interface {
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error)
|
||||
ApproveStaffPurchase(ctx *fiber.Ctx, id uint64, req *validation.ApproveStaffPurchaseRequest) (*entity.Purchase, error)
|
||||
}
|
||||
|
||||
type purchaseService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
PurchaseRepo rPurchase.PurchaseRepository
|
||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||
WarehouseRepo rWarehouse.WarehouseRepository
|
||||
SupplierRepo rSupplier.SupplierRepository
|
||||
ApprovalRepo commonRepo.ApprovalRepository
|
||||
}
|
||||
|
||||
func NewPurchaseService(
|
||||
validate *validator.Validate,
|
||||
purchaseRepo rPurchase.PurchaseRepository,
|
||||
productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
|
||||
warehouseRepo rWarehouse.WarehouseRepository,
|
||||
supplierRepo rSupplier.SupplierRepository,
|
||||
approvalRepo commonRepo.ApprovalRepository,
|
||||
) PurchaseService {
|
||||
return &purchaseService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
PurchaseRepo: purchaseRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
SupplierRepo: supplierRepo,
|
||||
ApprovalRepo: approvalRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func uint64Ptr(v uint64) *uint64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
supplier, err := s.SupplierRepo.GetByID(c.Context(), req.SupplierID, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Supplier not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to get supplier: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get supplier")
|
||||
}
|
||||
|
||||
warehouse, err := s.WarehouseRepo.GetDetailByID(c.Context(), req.WarehouseID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to get warehouse: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
||||
}
|
||||
|
||||
if warehouse.AreaId != req.AreaID {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse does not belong to the provided area")
|
||||
}
|
||||
if warehouse.LocationId == nil || *warehouse.LocationId != req.LocationID {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse does not belong to the provided location")
|
||||
}
|
||||
|
||||
type aggregatedItem struct {
|
||||
productId uint64
|
||||
warehouseId uint64
|
||||
productWarehouseId *uint64
|
||||
subQty float64
|
||||
}
|
||||
|
||||
if len(req.Items) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Items must not be empty")
|
||||
}
|
||||
|
||||
aggregated := make([]*aggregatedItem, 0, len(req.Items))
|
||||
indexMap := make(map[string]int)
|
||||
|
||||
for _, item := range req.Items {
|
||||
var (
|
||||
productId = uint64(item.ProductID)
|
||||
warehouseId = uint64(req.WarehouseID)
|
||||
productWarehouseId *uint64
|
||||
)
|
||||
|
||||
if item.ProductWarehouseID != nil {
|
||||
productWarehouse, err := s.ProductWarehouseRepo.GetDetailByID(c.Context(), *item.ProductWarehouseID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", *item.ProductWarehouseID))
|
||||
}
|
||||
s.Log.Errorf("Failed to get product warehouse %d: %+v", *item.ProductWarehouseID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||
}
|
||||
|
||||
if productWarehouse.WarehouseId != req.WarehouseID {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Product warehouse does not match selected warehouse")
|
||||
}
|
||||
|
||||
productId = uint64(productWarehouse.ProductId)
|
||||
warehouseId = uint64(productWarehouse.WarehouseId)
|
||||
idCopy := uint64(productWarehouse.Id)
|
||||
productWarehouseId = &idCopy
|
||||
} else {
|
||||
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), item.ProductID, req.WarehouseID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to get product warehouse for product %d and warehouse %d: %+v", item.ProductID, req.WarehouseID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||
}
|
||||
if err == nil {
|
||||
idCopy := uint64(productWarehouse.Id)
|
||||
productWarehouseId = &idCopy
|
||||
}
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%d:%d", productId, warehouseId)
|
||||
if idx, ok := indexMap[key]; ok {
|
||||
aggregated[idx].subQty += item.Quantity
|
||||
continue
|
||||
}
|
||||
|
||||
entry := &aggregatedItem{
|
||||
productId: productId,
|
||||
warehouseId: warehouseId,
|
||||
productWarehouseId: productWarehouseId,
|
||||
subQty: item.Quantity,
|
||||
}
|
||||
aggregated = append(aggregated, entry)
|
||||
indexMap[key] = len(aggregated) - 1
|
||||
}
|
||||
|
||||
prNumber := fmt.Sprintf("PR-%s-%s", time.Now().Format("20060102"), uuid.NewString()[:8])
|
||||
|
||||
var creditTerm *int
|
||||
var dueDate *time.Time
|
||||
|
||||
if supplier.DueDate > 0 {
|
||||
ct := supplier.DueDate
|
||||
creditTerm = &ct
|
||||
d := time.Now().UTC().AddDate(0, 0, ct)
|
||||
dueDate = &d
|
||||
}
|
||||
|
||||
purchase := &entity.Purchase{
|
||||
PrNumber: prNumber,
|
||||
SupplierId: uint64(req.SupplierID),
|
||||
CreditTerm: creditTerm,
|
||||
DueDate: dueDate,
|
||||
GrandTotal: 0,
|
||||
Notes: req.Notes,
|
||||
CreatedBy: 1, // TODO: replace with authenticated user id once available
|
||||
}
|
||||
|
||||
items := make([]*entity.PurchaseItem, 0, len(aggregated))
|
||||
for _, item := range aggregated {
|
||||
items = append(items, &entity.PurchaseItem{
|
||||
ProductId: item.productId,
|
||||
WarehouseId: item.warehouseId,
|
||||
ProductWarehouseId: item.productWarehouseId,
|
||||
SubQty: item.subQty,
|
||||
TotalQty: item.subQty,
|
||||
TotalUsed: 0,
|
||||
Price: 0,
|
||||
TotalPrice: 0,
|
||||
})
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
purchaseRepoTx := rPurchase.NewPurchaseRepository(tx)
|
||||
if err := purchaseRepoTx.CreateWithItems(ctx, purchase, items); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actorID := uint(purchase.CreatedBy)
|
||||
if actorID == 0 {
|
||||
actorID = 1
|
||||
}
|
||||
action := entity.ApprovalActionCreated
|
||||
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||
if _, err := approvalSvc.CreateApproval(
|
||||
ctx,
|
||||
utils.ApprovalWorkflowPurchase,
|
||||
uint(purchase.Id),
|
||||
utils.PurchaseStepPengajuan,
|
||||
&action,
|
||||
actorID,
|
||||
nil,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if transactionErr != nil {
|
||||
s.Log.Errorf("Failed to create purchase requisition: %+v", transactionErr)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create purchase requisition")
|
||||
}
|
||||
|
||||
created, err := s.PurchaseRepo.GetByIDWithRelations(ctx, purchase.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to load created purchase requisition: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase requisition")
|
||||
}
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint64, req *validation.ApproveStaffPurchaseRequest) (*entity.Purchase, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase requisition not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to get purchase requisition: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase requisition")
|
||||
}
|
||||
|
||||
if len(purchase.Items) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase requisition has no items to approve")
|
||||
}
|
||||
|
||||
requestItems := make(map[uint64]validation.StaffPurchaseApprovalItem, len(req.Items))
|
||||
for _, item := range req.Items {
|
||||
requestItems[item.PurchaseItemID] = item
|
||||
}
|
||||
|
||||
updates := make([]rPurchase.PurchasePricingUpdate, 0, len(purchase.Items))
|
||||
var grandTotal float64
|
||||
|
||||
for _, item := range purchase.Items {
|
||||
data, ok := requestItems[item.Id]
|
||||
if !ok {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Missing pricing data for item %d", item.Id))
|
||||
}
|
||||
delete(requestItems, item.Id)
|
||||
|
||||
if data.Price <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Price for item %d must be greater than 0", item.Id))
|
||||
}
|
||||
|
||||
totalPrice := data.TotalPrice
|
||||
if totalPrice == nil {
|
||||
calculated := data.Price * item.TotalQty
|
||||
totalPrice = &calculated
|
||||
}
|
||||
if *totalPrice <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total price for item %d must be greater than 0", item.Id))
|
||||
}
|
||||
|
||||
updates = append(updates, rPurchase.PurchasePricingUpdate{
|
||||
ItemID: item.Id,
|
||||
Price: data.Price,
|
||||
TotalPrice: *totalPrice,
|
||||
})
|
||||
grandTotal += *totalPrice
|
||||
}
|
||||
|
||||
if len(requestItems) > 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Found pricing data for items that do not belong to this purchase requisition")
|
||||
}
|
||||
|
||||
action := entity.ApprovalActionApproved
|
||||
actorID := uint(1) // TODO: replace with authenticated user id once available
|
||||
|
||||
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
purchaseRepoTx := rPurchase.NewPurchaseRepository(tx)
|
||||
if err := purchaseRepoTx.UpdatePricing(ctx, purchase.Id, updates, grandTotal); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||
if _, err := approvalSvc.CreateApproval(
|
||||
ctx,
|
||||
utils.ApprovalWorkflowPurchase,
|
||||
uint(purchase.Id),
|
||||
utils.PurchaseStepStaffPurchase,
|
||||
&action,
|
||||
actorID,
|
||||
req.Notes,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if transactionErr != nil {
|
||||
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to approve purchase requisition %d: %+v", purchase.Id, transactionErr)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to approve purchase requisition")
|
||||
}
|
||||
|
||||
updated, err := s.PurchaseRepo.GetByIDWithRelations(ctx, purchase.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to load purchase requisition after approval: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase requisition")
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package validation
|
||||
|
||||
type PurchaseItemPayload struct {
|
||||
ProductID uint `json:"product_id" validate:"required"`
|
||||
ProductWarehouseID *uint `json:"product_warehouse_id,omitempty" validate:"omitempty,gt=0"`
|
||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
type CreatePurchaseRequest struct {
|
||||
SupplierID uint `json:"supplier_id" validate:"required"`
|
||||
AreaID uint `json:"area_id" validate:"required"`
|
||||
LocationID uint `json:"location_id" validate:"required"`
|
||||
WarehouseID uint `json:"warehouse_id" validate:"required"`
|
||||
Notes *string `json:"notes" validate:"omitempty,max=500"`
|
||||
Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type StaffPurchaseApprovalItem struct {
|
||||
PurchaseItemID uint64 `json:"purchase_item_id" validate:"required,gt=0"`
|
||||
Price float64 `json:"price" validate:"required,gt=0"`
|
||||
TotalPrice *float64 `json:"total_price,omitempty" validate:"omitempty,gt=0"`
|
||||
}
|
||||
|
||||
type ApproveStaffPurchaseRequest struct {
|
||||
Items []StaffPurchaseApprovalItem `json:"items" validate:"required,min=1,dive"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||
}
|
||||
@@ -8,12 +8,13 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
approvals "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals"
|
||||
constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants"
|
||||
inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory"
|
||||
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
|
||||
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
|
||||
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
|
||||
approvals "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals"
|
||||
purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases"
|
||||
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users"
|
||||
// MODULE IMPORTS
|
||||
)
|
||||
|
||||
@@ -30,6 +31,7 @@ func Routes(app *fiber.App, db *gorm.DB) {
|
||||
inventory.InventoryModule{},
|
||||
production.ProductionModule{},
|
||||
approvals.ApprovalModule{},
|
||||
purchases.PurchaseModule{},
|
||||
// MODULE REGISTRY
|
||||
}
|
||||
|
||||
|
||||
@@ -209,6 +209,21 @@ var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||
RecordingStepDisetujui: "Disetujui",
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Purchase Approval
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const (
|
||||
ApprovalWorkflowPurchase approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PURCHASES")
|
||||
PurchaseStepPengajuan approvalutils.ApprovalStep = 1
|
||||
PurchaseStepStaffPurchase approvalutils.ApprovalStep = 2
|
||||
)
|
||||
|
||||
var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||
PurchaseStepPengajuan: "Pengajuan",
|
||||
PurchaseStepStaffPurchase: "Staff Purchase",
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Validators
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -12,7 +12,10 @@ func MapBodyWeights(recordingID uint, items []validation.BodyWeight) []entity.Re
|
||||
|
||||
result := make([]entity.RecordingBW, 0, len(items))
|
||||
for _, item := range items {
|
||||
totalWeight := item.TotalWeight
|
||||
var totalWeight float64
|
||||
if item.TotalWeight != nil {
|
||||
totalWeight = *item.TotalWeight
|
||||
}
|
||||
if totalWeight <= 0 {
|
||||
totalWeight = item.AvgWeight * item.Qty
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user