unfinished purchase

This commit is contained in:
ragilap
2025-11-05 18:58:06 +07:00
parent 4aed480662
commit 8f74391f1e
23 changed files with 1155 additions and 52 deletions
@@ -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);
+26
View File
@@ -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"`
}
+31
View File
@@ -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 { type ApprovalBaseDTO struct {
Id uint `json:"id"`
StepNumber uint16 `json:"step_number"` StepNumber uint16 `json:"step_number"`
StepName string `json:"step_name"` StepName string `json:"step_name"`
Action *string `json:"action"` Action *string `json:"action"`
@@ -27,6 +28,7 @@ type ApprovalGroupDTO struct {
func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO { func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO {
dto := ApprovalBaseDTO{ dto := ApprovalBaseDTO{
Id: e.Id,
Notes: e.Notes, Notes: e.Notes,
} }
@@ -23,6 +23,7 @@ type ProductWarehouseRepository interface {
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error) GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error 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 { type ProductWarehouseRepositoryImpl struct {
@@ -149,6 +150,20 @@ func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, d
return nil 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) { func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) {
var product entity.Product var product entity.Product
err := r.DB().WithContext(ctx). err := r.DB().WithContext(ctx).
@@ -17,6 +17,7 @@ type WarehouseRepository interface {
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error)
GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
GetLatestByKandangID(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 { type WarehouseRepositoryImpl struct {
@@ -62,6 +63,18 @@ func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId
return &warehouse, nil 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) { func (r *WarehouseRepositoryImpl) GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) {
var warehouse entity.Warehouse var warehouse entity.Warehouse
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
@@ -12,7 +12,7 @@ import (
func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.ProjectflockService) { func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.ProjectflockService) {
ctrl := controller.NewProjectflockController(s) ctrl := controller.NewProjectflockController(s)
route := v1.Group("/project_flocks") route := v1.Group("/project-flocks")
// route.Get("/", m.Auth(u), ctrl.GetAll) // route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne) // 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.Delete("/:id", ctrl.DeleteOne)
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
route.Post("/approvals", ctrl.Approval) 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) { func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) {
var population entity.ProjectFlockPopulation var total float64
err := tx. err := tx.
Where("project_flock_kandang_id = ?", projectFlockKandangId). Table("project_flock_populations").
Order("created_at DESC"). Select("COALESCE(SUM(project_flock_populations.total_qty - project_flock_populations.total_used_qty), 0) AS total_qty").
First(&population).Error Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id").
if errors.Is(err, gorm.ErrRecordNotFound) { Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangId).
return 0, nil Scan(&total).Error
}
if err != nil { if err != nil {
return 0, err 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) { 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 return nil, err
} }
if req.BodyWeights == nil && req.Stocks == nil && req.Depletions == nil && req.Eggs == nil {
return s.GetOne(c, id)
}
ctx := c.Context() ctx := c.Context()
var recordingEntity *entity.Recording var recordingEntity *entity.Recording
@@ -277,12 +281,21 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
} }
recordingEntity = recording 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 var category string
if recordingEntity.ProjectFlockKandang != nil && recordingEntity.ProjectFlockKandang.ProjectFlock.Id != 0 { if recordingEntity.ProjectFlockKandang != nil && recordingEntity.ProjectFlockKandang.ProjectFlock.Id != 0 {
category = strings.ToUpper(recordingEntity.ProjectFlockKandang.ProjectFlock.Category) category = strings.ToUpper(recordingEntity.ProjectFlockKandang.ProjectFlock.Category)
} }
isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying)) isLaying := category == strings.ToUpper(string(utils.ProjectFlockCategoryLaying))
if req.Eggs != nil { if hasEggChanges {
if !isLaying && len(req.Eggs) > 0 { if !isLaying && len(req.Eggs) > 0 {
return fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks") 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 { if err := s.Repository.DeleteBodyWeights(tx, recordingEntity.Id); err != nil {
s.Log.Errorf("Failed to clear body weights: %+v", err) s.Log.Errorf("Failed to clear body weights: %+v", err)
return err return err
@@ -302,11 +337,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
} }
} }
if req.Stocks != nil { if hasStockChanges {
if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil {
return err
}
existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id) existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id)
if err != nil { if err != nil {
s.Log.Errorf("Failed to list existing stocks: %+v", err) 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 hasDepletionChanges {
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
}
existingDepletions, err := s.Repository.ListDepletions(tx, recordingEntity.Id) existingDepletions, err := s.Repository.ListDepletions(tx, recordingEntity.Id)
if err != nil { if err != nil {
s.Log.Errorf("Failed to list existing depletions: %+v", err) 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) existingEggs, err := s.Repository.ListEggs(tx, recordingEntity.Id)
if err != nil { if err != nil {
s.Log.Errorf("Failed to list existing eggs: %+v", err) s.Log.Errorf("Failed to list existing eggs: %+v", err)
@@ -386,17 +407,71 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err) s.Log.Errorf("Failed to adjust product warehouses for eggs: %+v", err)
return err return err
} }
hasExistingGradings = false
hasEggsAfterUpdate = len(req.Eggs) > 0
} }
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil { if hasBodyChanges || hasStockChanges || hasDepletionChanges {
s.Log.Errorf("Failed to recompute recording metrics: %+v", err) if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
return err s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
return err
}
} }
action := entity.ApprovalActionUpdated action := entity.ApprovalActionUpdated
if err := s.createRecordingApproval(ctx, tx, recordingEntity.Id, utils.RecordingStepPengajuan, action, recordingEntity.CreatedBy, nil); err != nil { actorID := recordingEntity.CreatedBy
s.Log.Errorf("Failed to create approval after recording update %d: %+v", recordingEntity.Id, err) if actorID == 0 {
return err 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 return nil
@@ -1015,13 +1090,21 @@ func (s *recordingService) ensureChickInExists(ctx context.Context, projectFlock
return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid") return fiber.NewError(fiber.StatusBadRequest, "Project flock kandang tidak valid")
} }
_, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID) populations, err := s.ProjectFlockPopulationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID)
if err == nil { if err != nil {
return 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 errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusBadRequest, "Project flock belum melakukan chick in sehingga belum dapat membuat recording") if len(populations) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Project flock belum memiliki chick in yang disetujui sehingga belum dapat membuat recording")
} }
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") 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")
} }
@@ -2,9 +2,9 @@ package validation
type ( type (
BodyWeight struct { BodyWeight struct {
AvgWeight float64 `json:"avg_weight" validate:"required"` AvgWeight float64 `json:"avg_weight" validate:"required"`
Qty float64 `json:"qty" validate:"required,gt=0"` 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 { 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,
}
}
+13
View File
@@ -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
}
+59
View File
@@ -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"`
}
+6 -4
View File
@@ -8,12 +8,13 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gorm.io/gorm" "gorm.io/gorm"
approvals "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals"
constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants" constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants"
inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory" inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory"
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master" master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
production "gitlab.com/mbugroup/lti-api.git/internal/modules/production"
purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases"
users "gitlab.com/mbugroup/lti-api.git/internal/modules/users" 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"
// MODULE IMPORTS // MODULE IMPORTS
) )
@@ -28,8 +29,9 @@ func Routes(app *fiber.App, db *gorm.DB) {
master.MasterModule{}, master.MasterModule{},
constants.ConstantModule{}, constants.ConstantModule{},
inventory.InventoryModule{}, inventory.InventoryModule{},
production.ProductionModule{}, production.ProductionModule{},
approvals.ApprovalModule{}, approvals.ApprovalModule{},
purchases.PurchaseModule{},
// MODULE REGISTRY // MODULE REGISTRY
} }
+17 -2
View File
@@ -205,8 +205,23 @@ const (
var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{ var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{
RecordingStepGradingTelur: "Grading-Telur", RecordingStepGradingTelur: "Grading-Telur",
RecordingStepPengajuan: "Pengajuan", RecordingStepPengajuan: "Pengajuan",
RecordingStepDisetujui: "Disetujui", 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",
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------
+4 -1
View File
@@ -12,7 +12,10 @@ func MapBodyWeights(recordingID uint, items []validation.BodyWeight) []entity.Re
result := make([]entity.RecordingBW, 0, len(items)) result := make([]entity.RecordingBW, 0, len(items))
for _, item := range items { for _, item := range items {
totalWeight := item.TotalWeight var totalWeight float64
if item.TotalWeight != nil {
totalWeight = *item.TotalWeight
}
if totalWeight <= 0 { if totalWeight <= 0 {
totalWeight = item.AvgWeight * item.Qty totalWeight = item.AvgWeight * item.Qty
} }