diff --git a/internal/entities/purchase_item.go b/internal/entities/purchase_item.go index 724c6376..a29d4173 100644 --- a/internal/entities/purchase_item.go +++ b/internal/entities/purchase_item.go @@ -21,6 +21,7 @@ type PurchaseItem struct { Price float64 `gorm:"type:numeric(15,3);default:0"` TotalPrice float64 `gorm:"type:numeric(15,3);default:0"` ExpenseNonstockId *uint64 + HasChickin bool `gorm:"-" json:"-"` // Relations ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"` diff --git a/internal/modules/purchases/dto/purchase.dto.go b/internal/modules/purchases/dto/purchase.dto.go index c8df2294..849ffd63 100644 --- a/internal/modules/purchases/dto/purchase.dto.go +++ b/internal/modules/purchases/dto/purchase.dto.go @@ -67,6 +67,7 @@ type PurchaseItemDTO struct { VehicleNumber *string `json:"vehicle_number"` TransportPerItem *float64 `json:"transport_per_item,omitempty"` ExpeditionVendor *supplierDTO.SupplierRelationDTO `json:"expedition_vendor,omitempty"` + HasChickin bool `json:"has_chickin"` } type PoExpeditionDTO struct { @@ -100,6 +101,7 @@ func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO { TravelNumber: item.TravelNumber, TravelDocumentPath: item.TravelNumberDocs, VehicleNumber: item.VehicleNumber, + HasChickin: item.HasChickin, } if item.Product != nil && item.Product.Id != 0 { diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 703c04b9..6ecc6e1f 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -255,6 +255,39 @@ func (s *purchaseService) GetOne(c *fiber.Ctx, id uint) (*entity.Purchase, error if err := s.attachLatestApproval(c.Context(), purchase); err != nil { s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err) } + if len(purchase.Items) > 0 { + itemIDs := make([]uint, 0, len(purchase.Items)) + for i := range purchase.Items { + if purchase.Items[i].Id == 0 { + continue + } + itemIDs = append(itemIDs, purchase.Items[i].Id) + } + if len(itemIDs) > 0 { + var usedIDs []uint + if err := s.PurchaseRepo.DB().WithContext(c.Context()). + Model(&entity.StockAllocation{}). + Distinct("stockable_id"). + Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?", + fifo.StockableKeyPurchaseItems.String(), + itemIDs, + fifo.UsableKeyProjectChickin.String(), + []string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending}, + ). + Pluck("stockable_id", &usedIDs).Error; err != nil { + return nil, err + } + usedSet := make(map[uint]struct{}, len(usedIDs)) + for _, id := range usedIDs { + usedSet[id] = struct{}{} + } + for i := range purchase.Items { + if _, ok := usedSet[purchase.Items[i].Id]; ok { + purchase.Items[i].HasChickin = true + } + } + } + } s.applyTravelDocumentURLs(c.Context(), purchase) return purchase, nil @@ -498,6 +531,54 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid return nil, utils.BadRequest("Items must not be empty for staff approval") } + if action == entity.ApprovalActionApproved { + itemIDs := make([]uint, 0, len(purchase.Items)) + itemByID := make(map[uint]entity.PurchaseItem, len(purchase.Items)) + for i := range purchase.Items { + if purchase.Items[i].Id == 0 { + continue + } + itemIDs = append(itemIDs, purchase.Items[i].Id) + itemByID[purchase.Items[i].Id] = purchase.Items[i] + } + if len(itemIDs) > 0 { + var usedIDs []uint + if err := s.PurchaseRepo.DB().WithContext(ctx). + Model(&entity.StockAllocation{}). + Distinct("stockable_id"). + Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?", + fifo.StockableKeyPurchaseItems.String(), + itemIDs, + fifo.UsableKeyProjectChickin.String(), + []string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending}, + ). + Pluck("stockable_id", &usedIDs).Error; err != nil { + return nil, err + } + if len(usedIDs) > 0 { + usedSet := make(map[uint]struct{}, len(usedIDs)) + for _, id := range usedIDs { + usedSet[id] = struct{}{} + } + for _, payload := range req.Items { + if payload.PurchaseItemID == 0 || payload.Qty == nil { + continue + } + if _, used := usedSet[payload.PurchaseItemID]; !used { + continue + } + item, ok := itemByID[payload.PurchaseItemID] + if !ok { + continue + } + if *payload.Qty != item.SubQty { + return nil, utils.BadRequest("Purchase sudah chickin, qty tidak bisa diubah") + } + } + } + } + } + payload, err := s.buildStaffAdjustmentPayload(c.Context(), purchase, req, syncReceiving) if err != nil { return nil, err @@ -745,6 +826,54 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation req.Items[idx].TravelDocumentPath = &uploadedURL } } + if action == entity.ApprovalActionApproved { + itemIDs := make([]uint, 0, len(purchase.Items)) + itemByID := make(map[uint]entity.PurchaseItem, len(purchase.Items)) + for i := range purchase.Items { + if purchase.Items[i].Id == 0 { + continue + } + itemIDs = append(itemIDs, purchase.Items[i].Id) + itemByID[purchase.Items[i].Id] = purchase.Items[i] + } + if len(itemIDs) > 0 { + var usedIDs []uint + if err := s.PurchaseRepo.DB().WithContext(ctx). + Model(&entity.StockAllocation{}). + Distinct("stockable_id"). + Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?", + fifo.StockableKeyPurchaseItems.String(), + itemIDs, + fifo.UsableKeyProjectChickin.String(), + []string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending}, + ). + Pluck("stockable_id", &usedIDs).Error; err != nil { + return nil, err + } + if len(usedIDs) > 0 { + usedSet := make(map[uint]struct{}, len(usedIDs)) + for _, id := range usedIDs { + usedSet[id] = struct{}{} + } + for _, payload := range req.Items { + if _, used := usedSet[payload.PurchaseItemID]; !used { + continue + } + item, ok := itemByID[payload.PurchaseItemID] + if !ok { + continue + } + receivedQty := item.SubQty + if payload.ReceivedQty != nil { + receivedQty = *payload.ReceivedQty + } + if receivedQty != item.TotalQty { + return nil, utils.BadRequest("Purchase sudah chickin, qty penerimaan tidak bisa diubah") + } + } + } + } + } itemMap := make(map[uint]*entity.PurchaseItem, len(purchase.Items)) for i := range purchase.Items { @@ -1367,6 +1496,30 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error { } transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + itemIDs := make([]uint, 0, len(itemsToDelete)) + for _, item := range itemsToDelete { + if item.Id == 0 { + continue + } + itemIDs = append(itemIDs, item.Id) + } + if len(itemIDs) > 0 { + var count int64 + if err := tx.Model(&entity.StockAllocation{}). + Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?", + fifo.StockableKeyPurchaseItems.String(), + itemIDs, + fifo.UsableKeyProjectChickin.String(), + []string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending}, + ). + Count(&count).Error; err != nil { + return err + } + if count > 0 { + return utils.BadRequest("Purchase already chickin, failed to delete purchase") + } + } + if err := s.rollbackPurchaseStock(ctx, tx, itemsToDelete, note, actorID); err != nil { return err } @@ -1383,6 +1536,10 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error { return nil }) if transactionErr != nil { + var fe *fiber.Error + if errors.As(transactionErr, &fe) { + return fe + } if errors.Is(transactionErr, gorm.ErrRecordNotFound) { return utils.NotFound("Purchase not found") }