|
|
|
@@ -99,6 +99,7 @@ func (s *purchaseService) withRelations(db *gorm.DB) *gorm.DB {
|
|
|
|
|
|
|
|
|
|
return db.
|
|
|
|
|
Preload("Supplier").
|
|
|
|
|
Preload("CreatedUser").
|
|
|
|
|
Preload("Items", func(db *gorm.DB) *gorm.DB {
|
|
|
|
|
return db.Order("id ASC")
|
|
|
|
|
}).
|
|
|
|
@@ -109,7 +110,10 @@ func (s *purchaseService) withRelations(db *gorm.DB) *gorm.DB {
|
|
|
|
|
Preload("Items.Product.Flags").
|
|
|
|
|
Preload("Items.Warehouse.Area").
|
|
|
|
|
Preload("Items.Warehouse.Location").
|
|
|
|
|
Preload("Items.ProductWarehouse")
|
|
|
|
|
Preload("Items.ProductWarehouse").
|
|
|
|
|
Preload("Items.ExpenseNonstock").
|
|
|
|
|
Preload("Items.ExpenseNonstock.Expense").
|
|
|
|
|
Preload("Items.ExpenseNonstock.Expense.Supplier")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Purchase, int64, error) {
|
|
|
|
@@ -121,7 +125,7 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|
|
|
|
|
|
|
|
|
createdFrom, createdTo, err := utils.ParseDateRangeForQuery(params.CreatedFrom, params.CreatedTo)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, 0, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
|
|
|
|
return nil, 0, utils.BadRequest(err.Error())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
purchases, total, err := s.PurchaseRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
|
|
|
@@ -180,7 +184,7 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.Log.Errorf("Failed to get purchases: %+v", err)
|
|
|
|
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchases")
|
|
|
|
|
return nil, 0, utils.Internal("Failed to get purchases")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i := range purchases {
|
|
|
|
@@ -193,19 +197,7 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *purchaseService) GetOne(c *fiber.Ctx, id uint) (*entity.Purchase, error) {
|
|
|
|
|
purchase, err := s.PurchaseRepo.GetByID(c.Context(), id, s.withRelations)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
|
|
|
|
}
|
|
|
|
|
s.Log.Errorf("Failed to get purchase: %+v", err)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.attachLatestApproval(c.Context(), purchase); err != nil {
|
|
|
|
|
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err)
|
|
|
|
|
}
|
|
|
|
|
return purchase, nil
|
|
|
|
|
return s.loadPurchase(c.Context(), id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error) {
|
|
|
|
@@ -220,10 +212,10 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|
|
|
|
|
|
|
|
|
if _, err := s.SupplierRepo.GetByID(c.Context(), req.SupplierID, nil); err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Supplier not found")
|
|
|
|
|
return nil, utils.NotFound("Supplier not found")
|
|
|
|
|
}
|
|
|
|
|
s.Log.Errorf("Failed to get supplier: %+v", err)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get supplier")
|
|
|
|
|
return nil, utils.Internal("Failed to get supplier")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type aggregatedItem struct {
|
|
|
|
@@ -234,7 +226,7 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(req.Items) == 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Items must not be empty")
|
|
|
|
|
return nil, utils.BadRequest("Items must not be empty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
warehouseCache := make(map[uint]*entity.Warehouse)
|
|
|
|
@@ -249,24 +241,27 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse %d not found", id))
|
|
|
|
|
return nil, nil, utils.NotFound(fmt.Sprintf("Warehouse %d not found", id))
|
|
|
|
|
}
|
|
|
|
|
s.Log.Errorf("Failed to get warehouse %d: %+v", id, err)
|
|
|
|
|
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
|
|
|
|
return nil, nil, utils.Internal("Failed to get warehouse")
|
|
|
|
|
}
|
|
|
|
|
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
|
|
|
|
return nil, nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse %d is not linked to a kandang", id))
|
|
|
|
|
return nil, nil, utils.BadRequest(fmt.Sprintf("Warehouse %d is not linked to a kandang", id))
|
|
|
|
|
}
|
|
|
|
|
var pfkID *uint
|
|
|
|
|
if s.ProjectFlockKandangRepo != nil {
|
|
|
|
|
if pfk, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(c.Context(), uint(*warehouse.KandangId)); err == nil && pfk != nil {
|
|
|
|
|
if pfk.ClosedAt != nil {
|
|
|
|
|
return nil, nil, utils.BadRequest("Project sudah closing")
|
|
|
|
|
}
|
|
|
|
|
idCopy := uint(pfk.Id)
|
|
|
|
|
pfkID = &idCopy
|
|
|
|
|
} else if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse %d has no active project flock", id))
|
|
|
|
|
return nil, nil, utils.BadRequest(fmt.Sprintf("Warehouse %d has no active project flock", id))
|
|
|
|
|
} else if err != nil {
|
|
|
|
|
s.Log.Errorf("Failed to validate project flock for warehouse %d: %+v", id, err)
|
|
|
|
|
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
|
|
|
|
return nil, nil, utils.Internal("Failed to validate project flock")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -287,10 +282,10 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|
|
|
|
linked, err := s.ProductRepo.IsLinkedToSupplier(c.Context(), item.ProductID, req.SupplierID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.Log.Errorf("Failed to validate product %d for supplier %d: %+v", item.ProductID, req.SupplierID, err)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product for supplier")
|
|
|
|
|
return nil, utils.Internal("Failed to validate product for supplier")
|
|
|
|
|
}
|
|
|
|
|
if !linked {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product %d is not linked to supplier %d", item.ProductID, req.SupplierID))
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Product %d is not linked to supplier %d", item.ProductID, req.SupplierID))
|
|
|
|
|
}
|
|
|
|
|
productSupplierCache[item.ProductID] = true
|
|
|
|
|
}
|
|
|
|
@@ -315,17 +310,13 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var dueDate *time.Time
|
|
|
|
|
if req.DueDate != nil && strings.TrimSpace(*req.DueDate) != "" {
|
|
|
|
|
parsed, err := utils.ParseDateString(strings.TrimSpace(*req.DueDate))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid due_date, expected YYYY-MM-DD")
|
|
|
|
|
}
|
|
|
|
|
parsed = parsed.UTC()
|
|
|
|
|
dueDate = &parsed
|
|
|
|
|
}
|
|
|
|
|
now := time.Now().UTC()
|
|
|
|
|
d := now.AddDate(0, 0, req.CreditTerm)
|
|
|
|
|
dueDate = &d
|
|
|
|
|
|
|
|
|
|
purchase := &entity.Purchase{
|
|
|
|
|
SupplierId: uint(req.SupplierID),
|
|
|
|
|
CreditTerm: req.CreditTerm,
|
|
|
|
|
DueDate: dueDate,
|
|
|
|
|
Notes: req.Notes,
|
|
|
|
|
CreatedBy: uint(actorID),
|
|
|
|
@@ -373,13 +364,13 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|
|
|
|
})
|
|
|
|
|
if transactionErr != nil {
|
|
|
|
|
s.Log.Errorf("Failed to create purchase: %+v", transactionErr)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create purchase")
|
|
|
|
|
return nil, utils.Internal("Failed to create purchase")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
created, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.Log.Errorf("Failed to load created purchase: %+v", err)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
|
|
|
|
return nil, utils.Internal("Failed to load purchase")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.attachLatestApproval(c.Context(), created); err != nil {
|
|
|
|
@@ -405,17 +396,15 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
|
|
|
|
purchase, err := s.loadPurchase(ctx, id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
|
|
|
|
}
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.attachLatestApproval(ctx, purchase); err != nil {
|
|
|
|
|
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
|
|
|
|
if action == entity.ApprovalActionApproved {
|
|
|
|
|
if err := s.ensureProjectFlockNotClosedForPurchase(ctx, purchase); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var latestStep uint16
|
|
|
|
@@ -429,7 +418,7 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
|
|
|
|
|
|
|
|
|
isInitialApproval := latestStep < uint16(utils.PurchaseStepStaffPurchase)
|
|
|
|
|
if isInitialApproval && latestStep != uint16(utils.PurchaseStepPengajuan) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase is not ready for staff approval")
|
|
|
|
|
return nil, utils.BadRequest("Purchase is not ready for staff approval")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hasReceivingData := false
|
|
|
|
@@ -442,8 +431,8 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
|
|
|
|
|
|
|
|
|
syncReceiving := !isInitialApproval && hasReceivingData
|
|
|
|
|
|
|
|
|
|
if len(req.Items) == 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Items must not be empty for staff approval")
|
|
|
|
|
if action == entity.ApprovalActionApproved && len(req.Items) == 0 {
|
|
|
|
|
return nil, utils.BadRequest("Items must not be empty for staff approval")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
payload, err := s.buildStaffAdjustmentPayload(c.Context(), purchase, req, syncReceiving)
|
|
|
|
@@ -491,18 +480,18 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
|
|
|
|
})
|
|
|
|
|
if transactionErr != nil {
|
|
|
|
|
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found")
|
|
|
|
|
return nil, utils.NotFound("Purchase item not found")
|
|
|
|
|
}
|
|
|
|
|
if isInitialApproval {
|
|
|
|
|
s.Log.Errorf("Failed to approve purchase %d: %+v", purchase.Id, transactionErr)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to approve purchase")
|
|
|
|
|
return nil, utils.Internal("Failed to approve purchase")
|
|
|
|
|
}
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update purchase pricing")
|
|
|
|
|
return nil, utils.Internal("Failed to update purchase pricing")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
|
|
|
|
return nil, utils.Internal("Failed to load purchase")
|
|
|
|
|
}
|
|
|
|
|
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
|
|
|
|
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
|
|
|
@@ -526,22 +515,19 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint, req *val
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
purchase, err := s.PurchaseRepo.GetByID(c.Context(), id, s.withRelations)
|
|
|
|
|
purchase, err := s.loadPurchase(c.Context(), id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if action == entity.ApprovalActionApproved {
|
|
|
|
|
if err := s.ensureProjectFlockNotClosedForPurchase(c.Context(), purchase); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
s.Log.Errorf("Failed to get purchase: %+v", err)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.attachLatestApproval(c.Context(), purchase); err != nil {
|
|
|
|
|
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if purchase.LatestApproval == nil ||
|
|
|
|
|
purchase.LatestApproval.StepNumber < uint16(utils.PurchaseStepStaffPurchase) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase must reach staff purchase step before manager approval")
|
|
|
|
|
return nil, utils.BadRequest("Purchase must reach staff purchase step before manager approval")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if action == entity.ApprovalActionRejected {
|
|
|
|
@@ -601,7 +587,7 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint, req *val
|
|
|
|
|
})
|
|
|
|
|
if transactionErr != nil {
|
|
|
|
|
s.Log.Errorf("Failed to approve manager purchase %d: %+v", purchase.Id, transactionErr)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate purchase order")
|
|
|
|
|
return nil, utils.Internal("Failed to generate purchase order")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if generatedNumber != "" {
|
|
|
|
@@ -612,7 +598,7 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint, req *val
|
|
|
|
|
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.Log.Errorf("Failed to load purchase after manager approval: %+v", err)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
|
|
|
|
return nil, utils.Internal("Failed to load purchase")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
|
|
|
@@ -639,29 +625,26 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
|
|
|
|
purchase, err := s.loadPurchase(ctx, id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
|
|
|
|
}
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase ")
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if purchase.PoNumber == nil || strings.TrimSpace(*purchase.PoNumber) == "" {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase order has not been generated")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.attachLatestApproval(c.Context(), purchase); err != nil {
|
|
|
|
|
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
|
|
|
|
return nil, utils.BadRequest("Purchase order has not been generated")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if purchase.LatestApproval == nil ||
|
|
|
|
|
purchase.LatestApproval.StepNumber < uint16(utils.PurchaseStepManager) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase must be approved by manager before receiving products")
|
|
|
|
|
return nil, utils.BadRequest("Purchase must be approved by manager before receiving products")
|
|
|
|
|
}
|
|
|
|
|
if action == entity.ApprovalActionApproved {
|
|
|
|
|
if err := s.ensureProjectFlockNotClosedForPurchase(ctx, purchase); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if action == entity.ApprovalActionApproved && len(req.Items) == 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Receiving data must not be empty")
|
|
|
|
|
return nil, utils.BadRequest("Receiving data must not be empty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if action == entity.ApprovalActionRejected {
|
|
|
|
@@ -670,7 +653,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|
|
|
|
}
|
|
|
|
|
updated, err := s.PurchaseRepo.GetByID(ctx, purchase.Id, s.withRelations)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
|
|
|
|
return nil, utils.Internal("Failed to load purchase")
|
|
|
|
|
}
|
|
|
|
|
if err := s.attachLatestApproval(ctx, updated); err != nil {
|
|
|
|
|
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
|
|
|
@@ -696,17 +679,22 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|
|
|
|
|
|
|
|
|
visitedItems := make(map[uint]struct{}, len(req.Items))
|
|
|
|
|
prepared := make([]preparedReceiving, 0, len(req.Items))
|
|
|
|
|
var earliestReceived *time.Time
|
|
|
|
|
for _, payload := range req.Items {
|
|
|
|
|
item, exists := itemMap[payload.PurchaseItemID]
|
|
|
|
|
if !exists {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Purchase item %d not found", payload.PurchaseItemID))
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Purchase item %d not found", payload.PurchaseItemID))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
receivedDate, err := utils.ParseDateString(payload.ReceivedDate)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid received_date for item %d", payload.PurchaseItemID))
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Invalid received_date for item %d", payload.PurchaseItemID))
|
|
|
|
|
}
|
|
|
|
|
receivedDate = receivedDate.UTC()
|
|
|
|
|
if earliestReceived == nil || receivedDate.Before(*earliestReceived) {
|
|
|
|
|
copy := receivedDate
|
|
|
|
|
earliestReceived = ©
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
warehouseID := uint(item.WarehouseId)
|
|
|
|
|
overrideWarehouse := false
|
|
|
|
@@ -715,10 +703,10 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|
|
|
|
overrideWarehouse = true
|
|
|
|
|
}
|
|
|
|
|
if warehouseID == 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse must be specified for item %d", payload.PurchaseItemID))
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Warehouse must be specified for item %d", payload.PurchaseItemID))
|
|
|
|
|
}
|
|
|
|
|
if payload.WarehouseID != nil && uint(item.WarehouseId) != warehouseID {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Receiving does not allow changing warehouse")
|
|
|
|
|
return nil, utils.BadRequest("Receiving does not allow changing warehouse")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var receivedQty float64
|
|
|
|
@@ -728,14 +716,14 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|
|
|
|
receivedQty = item.SubQty
|
|
|
|
|
}
|
|
|
|
|
if receivedQty < 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Received quantity for item %d cannot be negative", payload.PurchaseItemID))
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Received quantity for item %d cannot be negative", payload.PurchaseItemID))
|
|
|
|
|
}
|
|
|
|
|
if receivedQty > item.SubQty {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Received quantity for item %d cannot exceed ordered quantity (%.3f)", payload.PurchaseItemID, item.SubQty))
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Received quantity for item %d cannot exceed ordered quantity (%.3f)", payload.PurchaseItemID, item.SubQty))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, dup := visitedItems[payload.PurchaseItemID]; dup {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate receiving data for item %d", payload.PurchaseItemID))
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Duplicate receiving data for item %d", payload.PurchaseItemID))
|
|
|
|
|
}
|
|
|
|
|
visitedItems[payload.PurchaseItemID] = struct{}{}
|
|
|
|
|
|
|
|
|
@@ -747,7 +735,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|
|
|
|
var transportPerItem *float64
|
|
|
|
|
if payload.TransportPerItem != nil {
|
|
|
|
|
if *payload.TransportPerItem < 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("transport_per_item for item %d cannot be negative", payload.PurchaseItemID))
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("transport_per_item for item %d cannot be negative", payload.PurchaseItemID))
|
|
|
|
|
}
|
|
|
|
|
val := *payload.TransportPerItem
|
|
|
|
|
transportPerItem = &val
|
|
|
|
@@ -768,7 +756,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|
|
|
|
// Require receiving payload to cover all purchase items so that
|
|
|
|
|
// receiving cannot be submitted partially item-by-item.
|
|
|
|
|
if len(visitedItems) != len(itemMap) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Receiving data must be provided for all purchase items")
|
|
|
|
|
return nil, utils.BadRequest("Receiving data must be provided for all purchase items")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
receivingAction := action
|
|
|
|
@@ -792,7 +780,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.Log.Errorf("Failed to inspect receiving approval for purchase %d: %+v", purchase.Id, err)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record purchase receiving")
|
|
|
|
|
return nil, utils.Internal("Failed to record purchase receiving")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if latestReceiving != nil {
|
|
|
|
@@ -826,7 +814,13 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|
|
|
|
|
|
|
|
|
// Always ensure PW when qty > 0 so stockable has target.
|
|
|
|
|
if prep.receivedQty > 0 {
|
|
|
|
|
pwID, err := pwRepoTx.EnsureProductWarehouse(c.Context(), uint(item.ProductId), prep.warehouseID, purchase.CreatedBy)
|
|
|
|
|
pwID, err := pwRepoTx.EnsureProductWarehouse(
|
|
|
|
|
c.Context(),
|
|
|
|
|
uint(item.ProductId),
|
|
|
|
|
prep.warehouseID,
|
|
|
|
|
item.ProjectFlockKandangId,
|
|
|
|
|
purchase.CreatedBy,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
@@ -882,6 +876,16 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update due_date based on earliest received date when receiving approved.
|
|
|
|
|
if earliestReceived != nil {
|
|
|
|
|
due := earliestReceived.AddDate(0, 0, purchase.CreditTerm)
|
|
|
|
|
if err := tx.Model(&entity.Purchase{}).
|
|
|
|
|
Where("id = ?", purchase.Id).
|
|
|
|
|
Update("due_date", due).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if s.FifoSvc != nil {
|
|
|
|
|
for _, adj := range fifoAdds {
|
|
|
|
|
if adj.pwID == 0 || adj.qty <= 0 {
|
|
|
|
@@ -903,15 +907,15 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|
|
|
|
})
|
|
|
|
|
if transactionErr != nil {
|
|
|
|
|
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found for receiving")
|
|
|
|
|
return nil, utils.NotFound("Purchase item not found for receiving")
|
|
|
|
|
}
|
|
|
|
|
s.Log.Errorf("Failed to save purchase receiving %d: %+v", purchase.Id, transactionErr)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record purchase receiving")
|
|
|
|
|
return nil, utils.Internal("Failed to record purchase receiving")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase ")
|
|
|
|
|
return nil, utils.Internal("Failed to load purchase ")
|
|
|
|
|
}
|
|
|
|
|
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
|
|
|
|
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
|
|
|
@@ -936,7 +940,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|
|
|
|
if fe, ok := err.(*fiber.Error); ok {
|
|
|
|
|
return nil, fe
|
|
|
|
|
}
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to sync expense")
|
|
|
|
|
return nil, utils.Internal("Failed to sync expense")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create approvals only after expense sync succeeds
|
|
|
|
@@ -959,20 +963,23 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
|
|
|
|
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
|
|
|
|
return nil, utils.NotFound("Purchase not found")
|
|
|
|
|
}
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
|
|
|
|
return nil, utils.Internal("Failed to get purchase")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.attachLatestApproval(ctx, purchase); err != nil {
|
|
|
|
|
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
|
|
|
|
}
|
|
|
|
|
if err := s.ensureProjectFlockNotClosedForPurchase(ctx, purchase); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if purchase.LatestApproval == nil || purchase.LatestApproval.StepNumber == uint16(utils.PurchaseStepPengajuan) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase cannot delete items before staff purchase approval")
|
|
|
|
|
return nil, utils.BadRequest("Purchase cannot delete items before staff purchase approval")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(purchase.Items) == 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase has no items to delete")
|
|
|
|
|
return nil, utils.BadRequest("Purchase has no items to delete")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
requested := make(map[uint]struct{}, len(req.ItemIDs))
|
|
|
|
@@ -992,7 +999,7 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(toDelete) == 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Requested items were not found in this purchase")
|
|
|
|
|
return nil, utils.BadRequest("Requested items were not found in this purchase")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toDeleteSet := make(map[uint]struct{}, len(toDelete))
|
|
|
|
@@ -1007,7 +1014,7 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(purchase.Items)-len(toDelete) <= 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase must keep at least one item")
|
|
|
|
|
return nil, utils.BadRequest("Purchase must keep at least one item")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
|
|
@@ -1021,9 +1028,9 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
|
|
|
|
})
|
|
|
|
|
if transactionErr != nil {
|
|
|
|
|
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found")
|
|
|
|
|
return nil, utils.NotFound("Purchase item not found")
|
|
|
|
|
}
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete purchase items")
|
|
|
|
|
return nil, utils.Internal("Failed to delete purchase items")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(itemsToDelete) > 0 {
|
|
|
|
@@ -1032,13 +1039,13 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
|
|
|
|
if fe, ok := err.(*fiber.Error); ok {
|
|
|
|
|
return nil, fe
|
|
|
|
|
}
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to sync expense")
|
|
|
|
|
return nil, utils.Internal("Failed to sync expense")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updated, err := s.PurchaseRepo.GetByID(ctx, purchase.Id, s.withRelations)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
|
|
|
|
return nil, utils.Internal("Failed to load purchase")
|
|
|
|
|
}
|
|
|
|
|
if err := s.attachLatestApproval(ctx, updated); err != nil {
|
|
|
|
|
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
|
|
|
@@ -1049,16 +1056,16 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
|
|
|
|
|
|
|
|
|
func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
|
|
|
|
if id == 0 {
|
|
|
|
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
|
|
|
|
|
return utils.BadRequest("Invalid purchase id")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx := c.Context()
|
|
|
|
|
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
|
|
|
|
purchase, err := s.loadPurchase(ctx, id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
|
|
|
|
}
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if err := s.ensureProjectFlockNotClosedForPurchase(ctx, purchase); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
itemsToDelete := make([]entity.PurchaseItem, len(purchase.Items))
|
|
|
|
@@ -1080,9 +1087,9 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
|
|
|
|
})
|
|
|
|
|
if transactionErr != nil {
|
|
|
|
|
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
|
|
|
|
return fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
|
|
|
|
return utils.NotFound("Purchase not found")
|
|
|
|
|
}
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete purchase")
|
|
|
|
|
return utils.Internal("Failed to delete purchase")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(itemsToDelete) > 0 {
|
|
|
|
@@ -1091,7 +1098,7 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
|
|
|
|
if fe, ok := err.(*fiber.Error); ok {
|
|
|
|
|
return fe
|
|
|
|
|
}
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to sync expense")
|
|
|
|
|
return utils.Internal("Failed to sync expense")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1109,7 +1116,7 @@ func (s *purchaseService) createPurchaseApproval(
|
|
|
|
|
allowDuplicate bool,
|
|
|
|
|
) error {
|
|
|
|
|
if purchaseID == 0 {
|
|
|
|
|
return fiber.NewError(fiber.StatusBadRequest, "Purchase is invalid for approval")
|
|
|
|
|
return utils.BadRequest("Purchase is invalid for approval")
|
|
|
|
|
}
|
|
|
|
|
if actorID == 0 {
|
|
|
|
|
actorID = 1
|
|
|
|
@@ -1117,7 +1124,7 @@ func (s *purchaseService) createPurchaseApproval(
|
|
|
|
|
|
|
|
|
|
svc := s.approvalServiceForDB(db)
|
|
|
|
|
if svc == nil {
|
|
|
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Approval service not available")
|
|
|
|
|
return utils.Internal("Approval service not available")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
modifier := func(db *gorm.DB) *gorm.DB {
|
|
|
|
@@ -1175,7 +1182,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|
|
|
|
syncReceiving bool,
|
|
|
|
|
) (*staffAdjustmentPayload, error) {
|
|
|
|
|
if len(req.Items) == 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Items must not be empty")
|
|
|
|
|
return nil, utils.BadRequest("Items must not be empty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
requestItems := make(map[uint]validation.StaffPurchaseApprovalItem, len(req.Items))
|
|
|
|
@@ -1187,7 +1194,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, exists := requestItems[item.PurchaseItemID]; exists {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate pricing data for item %d", item.PurchaseItemID))
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Duplicate pricing data for item %d", item.PurchaseItemID))
|
|
|
|
|
}
|
|
|
|
|
requestItems[item.PurchaseItemID] = item
|
|
|
|
|
}
|
|
|
|
@@ -1205,34 +1212,31 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|
|
|
|
allowedWarehouses[item.WarehouseId] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
if len(allowedWarehouses) == 0 && len(newPayloads) > 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "No available warehouses for this purchase")
|
|
|
|
|
return nil, utils.BadRequest("No available warehouses for this purchase")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Missing pricing data for item %d", item.Id))
|
|
|
|
|
}
|
|
|
|
|
if data.ProductID != 0 && data.ProductID != item.ProductId {
|
|
|
|
|
return nil, fiber.NewError(
|
|
|
|
|
fiber.StatusBadRequest,
|
|
|
|
|
fmt.Sprintf("Cannot change product for item %d. Delete the item and create a new one instead", item.Id),
|
|
|
|
|
)
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Cannot change product for item %d. Delete the item and create a new one instead", item.Id))
|
|
|
|
|
}
|
|
|
|
|
if data.WarehouseID != 0 && data.WarehouseID != item.WarehouseId {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse mismatch for item %d", item.Id))
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Warehouse mismatch for item %d", item.Id))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
effectiveQty := item.SubQty
|
|
|
|
|
if data.Qty != nil {
|
|
|
|
|
if *data.Qty <= 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity for item %d must be greater than 0", item.Id))
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Quantity for item %d must be greater than 0", item.Id))
|
|
|
|
|
}
|
|
|
|
|
if item.TotalUsed > 0 && *data.Qty < item.TotalUsed {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity for item %d cannot be lower than used amount (%.3f)", item.Id, item.TotalUsed))
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Quantity for item %d cannot be lower than used amount (%.3f)", item.Id, item.TotalUsed))
|
|
|
|
|
}
|
|
|
|
|
if (item.TotalQty > 0 || item.TotalUsed > 0) && !syncReceiving {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot change quantity for item %d because it already has receiving data", item.Id))
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Cannot change quantity for item %d because it already has receiving data", item.Id))
|
|
|
|
|
}
|
|
|
|
|
effectiveQty = *data.Qty
|
|
|
|
|
}
|
|
|
|
@@ -1261,7 +1265,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|
|
|
|
delete(requestItems, item.Id)
|
|
|
|
|
}
|
|
|
|
|
if len(requestItems) > 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Found pricing data for items that do not belong to this purchase")
|
|
|
|
|
return nil, utils.BadRequest("Found pricing data for items that do not belong to this purchase")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
productSupplierCache := make(map[uint]bool)
|
|
|
|
@@ -1270,37 +1274,28 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|
|
|
|
|
|
|
|
|
for _, payload := range newPayloads {
|
|
|
|
|
if payload.ProductID == 0 || payload.WarehouseID == 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Product and warehouse must be provided for new items")
|
|
|
|
|
return nil, utils.BadRequest("Product and warehouse must be provided for new items")
|
|
|
|
|
}
|
|
|
|
|
if payload.Qty == nil || *payload.Qty <= 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity must be greater than 0 for product %d", payload.ProductID))
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Quantity must be greater than 0 for product %d", payload.ProductID))
|
|
|
|
|
}
|
|
|
|
|
if _, ok := allowedWarehouses[payload.WarehouseID]; !ok {
|
|
|
|
|
return nil, fiber.NewError(
|
|
|
|
|
fiber.StatusBadRequest,
|
|
|
|
|
fmt.Sprintf("Warehouse %d is not available for this purchase", payload.WarehouseID),
|
|
|
|
|
)
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Warehouse %d is not available for this purchase", payload.WarehouseID))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
key := fmt.Sprintf("%d:%d", payload.ProductID, payload.WarehouseID)
|
|
|
|
|
if _, exists := existingCombos[key]; exists {
|
|
|
|
|
return nil, fiber.NewError(
|
|
|
|
|
fiber.StatusBadRequest,
|
|
|
|
|
fmt.Sprintf("Product %d in warehouse %d already exists in this purchase", payload.ProductID, payload.WarehouseID),
|
|
|
|
|
)
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Product %d in warehouse %d already exists in this purchase", payload.ProductID, payload.WarehouseID))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, checked := productSupplierCache[payload.ProductID]; !checked {
|
|
|
|
|
linked, err := s.ProductRepo.IsLinkedToSupplier(ctx, uint(payload.ProductID), uint(purchase.SupplierId))
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.Log.Errorf("Failed to validate product %d for supplier %d: %+v", payload.ProductID, purchase.SupplierId, err)
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product for supplier")
|
|
|
|
|
return nil, utils.Internal("Failed to validate product for supplier")
|
|
|
|
|
}
|
|
|
|
|
if !linked {
|
|
|
|
|
return nil, fiber.NewError(
|
|
|
|
|
fiber.StatusBadRequest,
|
|
|
|
|
fmt.Sprintf("Product %d is not linked to supplier %d", payload.ProductID, purchase.SupplierId),
|
|
|
|
|
)
|
|
|
|
|
return nil, utils.BadRequest(fmt.Sprintf("Product %d is not linked to supplier %d", payload.ProductID, purchase.SupplierId))
|
|
|
|
|
}
|
|
|
|
|
productSupplierCache[payload.ProductID] = true
|
|
|
|
|
}
|
|
|
|
@@ -1328,7 +1323,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(updates) == 0 && len(newItems) == 0 {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase has no items to process")
|
|
|
|
|
return nil, utils.BadRequest("Purchase has no items to process")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &staffAdjustmentPayload{
|
|
|
|
@@ -1340,10 +1335,10 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|
|
|
|
// ? helper
|
|
|
|
|
func calculateTotalPrice(quantity float64, price float64, provided *float64, ref string) (float64, error) {
|
|
|
|
|
if quantity <= 0 {
|
|
|
|
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity for %s must be greater than 0", ref))
|
|
|
|
|
return 0, utils.BadRequest(fmt.Sprintf("Quantity for %s must be greater than 0", ref))
|
|
|
|
|
}
|
|
|
|
|
if price <= 0 {
|
|
|
|
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Price for %s must be greater than 0", ref))
|
|
|
|
|
return 0, utils.BadRequest(fmt.Sprintf("Price for %s must be greater than 0", ref))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expectedTotal := price * quantity
|
|
|
|
@@ -1352,10 +1347,10 @@ func calculateTotalPrice(quantity float64, price float64, provided *float64, ref
|
|
|
|
|
return expectedTotal, nil
|
|
|
|
|
}
|
|
|
|
|
if *provided <= 0 {
|
|
|
|
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total price for %s must be greater than 0", ref))
|
|
|
|
|
return 0, utils.BadRequest(fmt.Sprintf("Total price for %s must be greater than 0", ref))
|
|
|
|
|
}
|
|
|
|
|
if math.Abs(*provided-expectedTotal) > priceTolerance {
|
|
|
|
|
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total price for %s must equal quantity x price", ref))
|
|
|
|
|
return 0, utils.BadRequest(fmt.Sprintf("Total price for %s must equal quantity x price", ref))
|
|
|
|
|
}
|
|
|
|
|
return *provided, nil
|
|
|
|
|
}
|
|
|
|
@@ -1384,7 +1379,7 @@ func parseApprovalActionInput(raw string) (entity.ApprovalAction, error) {
|
|
|
|
|
case string(entity.ApprovalActionRejected):
|
|
|
|
|
return entity.ApprovalActionRejected, nil
|
|
|
|
|
default:
|
|
|
|
|
return "", fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
|
|
|
|
|
return "", utils.BadRequest("action must be APPROVED or REJECTED")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1399,13 +1394,58 @@ func (s *purchaseService) rejectAndReload(
|
|
|
|
|
if err := s.createPurchaseApproval(c.Context(), nil, purchaseID, step, entity.ApprovalActionRejected, actorID, notes, false); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchaseID, s.withRelations)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
|
|
|
|
}
|
|
|
|
|
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
|
|
|
|
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
|
|
|
|
}
|
|
|
|
|
return updated, nil
|
|
|
|
|
return s.loadPurchase(c.Context(), purchaseID)
|
|
|
|
|
}
|
|
|
|
|
func (s *purchaseService) loadPurchase(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
id uint,
|
|
|
|
|
) (*entity.Purchase, error) {
|
|
|
|
|
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
|
return nil, utils.NotFound("Purchase not found")
|
|
|
|
|
}
|
|
|
|
|
s.Log.Errorf("Failed to get purchase %d: %+v", id, err)
|
|
|
|
|
return nil, utils.Internal("Failed to get purchase")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.attachLatestApproval(ctx, purchase); err != nil {
|
|
|
|
|
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return purchase, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func collectPFKIDsFromPurchase(p *entity.Purchase) []uint {
|
|
|
|
|
seen := make(map[uint]struct{})
|
|
|
|
|
ids := make([]uint, 0)
|
|
|
|
|
|
|
|
|
|
for _, item := range p.Items {
|
|
|
|
|
if item.ProjectFlockKandangId == nil || *item.ProjectFlockKandangId == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
id := uint(*item.ProjectFlockKandangId)
|
|
|
|
|
if _, ok := seen[id]; ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen[id] = struct{}{}
|
|
|
|
|
ids = append(ids, id)
|
|
|
|
|
}
|
|
|
|
|
return ids
|
|
|
|
|
}
|
|
|
|
|
func (s *purchaseService) ensureProjectFlockNotClosedForPurchase(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
purchase *entity.Purchase,
|
|
|
|
|
) error {
|
|
|
|
|
pfkIDs := collectPFKIDsFromPurchase(purchase)
|
|
|
|
|
if len(pfkIDs) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db := s.PurchaseRepo.DB()
|
|
|
|
|
if db == nil {
|
|
|
|
|
return utils.Internal("DB not available for project flock validation")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(ctx, db, pfkIDs)
|
|
|
|
|
}
|
|
|
|
|