From 0396aa02554624fd89f45b3ea3a9d42a71bae93e Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 30 Dec 2025 14:27:50 +0700 Subject: [PATCH 1/4] feat(BE-287):adjustment purchase restrict unfinished --- .../purchases/services/expense_bridge.go | 44 +++++++++++-------- .../purchases/services/purchase.service.go | 31 +++++++------ 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/internal/modules/purchases/services/expense_bridge.go b/internal/modules/purchases/services/expense_bridge.go index 146f04f2..70a06c92 100644 --- a/internal/modules/purchases/services/expense_bridge.go +++ b/internal/modules/purchases/services/expense_bridge.go @@ -310,9 +310,6 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ return err } if cnt == 1 { - if item.Warehouse == nil || item.Warehouse.KandangId == nil || *item.Warehouse.KandangId == 0 { - return fiber.NewError(fiber.StatusBadRequest, "Warehouse not connect to kandangs") - } newNonstockID, err := b.findExpeditionNonstockID(ctx, supplierID) if err != nil { return err @@ -332,7 +329,9 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ "price": pricePerItem, "notes": note, "nonstock_id": newNonstockID, - "kandang_id": uint64(*item.Warehouse.KandangId), + } + if item.Warehouse != nil && item.Warehouse.KandangId != nil && *item.Warehouse.KandangId != 0 { + updateBody["kandang_id"] = uint64(*item.Warehouse.KandangId) } if err := b.db.WithContext(ctx). Model(&entity.ExpenseNonstock{}). @@ -550,18 +549,27 @@ func (b *expenseBridge) createExpenseViaService( } kandangID := items[0].kandangID - if kandangID == nil || *kandangID == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse not connect to kandangs") - } - - kandang, err := b.kandangRepo.GetByID(ctx, *kandangID, func(db *gorm.DB) *gorm.DB { - return db.Select("id, location_id") - }) - if err != nil { - return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID)) - } - if kandang == nil { - return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID)) + var locationID uint64 + var expenseKandangID *uint64 + if kandangID != nil && *kandangID != 0 { + kandang, err := b.kandangRepo.GetByID(ctx, *kandangID, func(db *gorm.DB) *gorm.DB { + return db.Select("id, location_id") + }) + if err != nil { + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID)) + } + if kandang == nil { + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Kandang not found: %d", *kandangID)) + } + locationID = uint64(kandang.LocationId) + id := uint64(*kandangID) + expenseKandangID = &id + } else { + warehouse := items[0].item.Warehouse + if warehouse == nil || warehouse.LocationId == nil || *warehouse.LocationId == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse location is required for expense") + } + locationID = uint64(*warehouse.LocationId) } costItems := make([]expenseValidation.CostItem, 0, len(items)) @@ -584,9 +592,9 @@ func (b *expenseBridge) createExpenseViaService( TransactionDate: utils.FormatDate(expenseDate), Category: "BOP", SupplierID: uint64(supplierID), - LocationID: uint64(kandang.LocationId), + LocationID: locationID, ExpenseNonstocks: []expenseValidation.ExpenseNonstock{{ - KandangID: func() *uint64 { id := uint64(*kandangID); return &id }(), + KandangID: expenseKandangID, CostItems: costItems, }}, } diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 366a8c0e..43c2bdc7 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -246,22 +246,25 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase s.Log.Errorf("Failed to get warehouse %d: %+v", id, err) return nil, nil, utils.Internal("Failed to get warehouse") } - if warehouse.KandangId == nil || *warehouse.KandangId == 0 { - return nil, nil, utils.BadRequest(fmt.Sprintf("%s is not linked to a kandang", warehouse.Name)) - } 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") + isKandang := strings.EqualFold(strings.TrimSpace(warehouse.Type), "KANDANG") + if isKandang { + if warehouse.KandangId == nil || *warehouse.KandangId == 0 { + return nil, nil, utils.BadRequest(fmt.Sprintf("%s is not linked to a kandang", warehouse.Name)) + } + 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, utils.BadRequest(fmt.Sprintf("%s has no active project flock", warehouse.Name)) + } else if err != nil { + s.Log.Errorf("Failed to validate project flock for warehouse %d: %+v", id, err) + return nil, nil, utils.Internal("Failed to validate project flock") } - idCopy := uint(pfk.Id) - pfkID = &idCopy - } else if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil, utils.BadRequest(fmt.Sprintf("%s has no active project flock", warehouse.Name)) - } else if err != nil { - s.Log.Errorf("Failed to validate project flock for warehouse %d: %+v", id, err) - return nil, nil, utils.Internal("Failed to validate project flock") } } From fd5f83ca58cc1c140a6e6977bee98f6f73b5031c Mon Sep 17 00:00:00 2001 From: ragilap Date: Wed, 31 Dec 2025 03:50:58 +0700 Subject: [PATCH 2/4] feat(BE-278): unrestrict feat warehouse purchase,adding purchase upload document --- internal/middleware/permissions.go | 1 + .../expenses/services/expense.service.go | 24 +++-- .../product_warehouse.repository.go | 25 +++++ .../controllers/purchase.controller.go | 36 +++++-- internal/modules/purchases/module.go | 1 + .../purchases/services/expense_bridge.go | 4 + .../purchases/services/purchase.service.go | 98 ++++++++++++++----- .../validations/purchase.validation.go | 29 +++--- internal/modules/repports/route.go | 2 +- internal/utils/constant.go | 2 + 10 files changed, 159 insertions(+), 63 deletions(-) diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index f0056149..1d308787 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -44,6 +44,7 @@ const ( P_ReportExpenseGetAll = "lti.repport.expense.list" P_ReportDeliveryGetAll = "lti.repport.delivery.list" P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list" + P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list" ) const ( diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index b4753451..37d4cec0 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -214,21 +214,19 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen return fiber.NewError(fiber.StatusInternalServerError, "Failed to get active project flocks for location") } - if len(activeProjectFlocks) == 0 { - return fiber.NewError(fiber.StatusBadRequest, "No active project flocks found for this location") - } + if len(activeProjectFlocks) > 0 { + projectFlockIDs := make([]uint64, len(activeProjectFlocks)) + for i, pf := range activeProjectFlocks { + projectFlockIDs[i] = uint64(pf.Id) + } - projectFlockIDs := make([]uint64, len(activeProjectFlocks)) - for i, pf := range activeProjectFlocks { - projectFlockIDs[i] = uint64(pf.Id) + projectFlockIdsJSON, err := json.Marshal(projectFlockIDs) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to marshal project_flock_ids") + } + jsonStr := string(projectFlockIdsJSON) + projectFlockIdJSON = &jsonStr } - - projectFlockIdsJSON, err := json.Marshal(projectFlockIDs) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to marshal project_flock_ids") - } - jsonStr := string(projectFlockIdsJSON) - projectFlockIdJSON = &jsonStr } expense = &entity.Expense{ diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index e759138e..3cb22851 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -199,6 +199,31 @@ func (r *ProductWarehouseRepositoryImpl) CleanupEmpty(ctx context.Context, affec return nil } + var inUseIDs []uint + if err := r.DB().WithContext(ctx). + Model(&entity.PurchaseItem{}). + Where("product_warehouse_id IN ?", emptyIDs). + Distinct(). + Pluck("product_warehouse_id", &inUseIDs).Error; err != nil { + return err + } + if len(inUseIDs) > 0 { + inUse := make(map[uint]struct{}, len(inUseIDs)) + for _, id := range inUseIDs { + inUse[id] = struct{}{} + } + filtered := make([]uint, 0, len(emptyIDs)) + for _, id := range emptyIDs { + if _, exists := inUse[id]; !exists { + filtered = append(filtered, id) + } + } + emptyIDs = filtered + } + if len(emptyIDs) == 0 { + return nil + } + if err := r.DB().WithContext(ctx). Model(&entity.PurchaseItem{}). Where("product_warehouse_id IN ?", emptyIDs). diff --git a/internal/modules/purchases/controllers/purchase.controller.go b/internal/modules/purchases/controllers/purchase.controller.go index b4cf5660..d9b32cd1 100644 --- a/internal/modules/purchases/controllers/purchase.controller.go +++ b/internal/modules/purchases/controllers/purchase.controller.go @@ -1,6 +1,7 @@ package controller import ( + "encoding/json" "fmt" "math" "strconv" @@ -24,13 +25,13 @@ func NewPurchaseController(s service.PurchaseService) *PurchaseController { func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error { query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - CreatedFrom: strings.TrimSpace(c.Query("created_from")), - CreatedTo: strings.TrimSpace(c.Query("created_to")), - SupplierID: uint(c.QueryInt("supplier_id", 0)), - AreaID: uint(c.QueryInt("area_id", 0)), - LocationID: uint(c.QueryInt("location_id", 0)), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + CreatedFrom: strings.TrimSpace(c.Query("created_from")), + CreatedTo: strings.TrimSpace(c.Query("created_to")), + SupplierID: uint(c.QueryInt("supplier_id", 0)), + AreaID: uint(c.QueryInt("area_id", 0)), + LocationID: uint(c.QueryInt("location_id", 0)), ProductCategoryID: uint(c.QueryInt("product_category_id", 0)), } @@ -86,7 +87,6 @@ func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error { 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 @@ -161,10 +161,26 @@ func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error { } req := new(validation.ReceivePurchaseRequest) - if err := c.BodyParser(req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + form, err := c.MultipartForm() + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form") + } + req.Action = c.FormValue("action") + if notes := strings.TrimSpace(c.FormValue("notes")); notes != "" { + req.Notes = ¬es } + itemsJSON := c.FormValue("items") + if strings.TrimSpace(itemsJSON) != "" { + if err := json.Unmarshal([]byte(itemsJSON), &req.Items); err != nil { + var singleItem validation.ReceivePurchaseItemRequest + if err := json.Unmarshal([]byte(itemsJSON), &singleItem); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid items JSON") + } + req.Items = []validation.ReceivePurchaseItemRequest{singleItem} + } + } + req.TravelDocuments = form.File["documents"] result, err := ctrl.service.ReceiveProducts(c, uint(id), req) if err != nil { return err diff --git a/internal/modules/purchases/module.go b/internal/modules/purchases/module.go index fa10559d..7e80de38 100644 --- a/internal/modules/purchases/module.go +++ b/internal/modules/purchases/module.go @@ -98,6 +98,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate approvalService, expenseBridge, fifoService, + documentSvc, ) userRepo := rUser.NewUserRepository(db) diff --git a/internal/modules/purchases/services/expense_bridge.go b/internal/modules/purchases/services/expense_bridge.go index 70a06c92..6c74a1fc 100644 --- a/internal/modules/purchases/services/expense_bridge.go +++ b/internal/modules/purchases/services/expense_bridge.go @@ -394,9 +394,13 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [ } if kandangID != nil { updateBody["kandang_id"] = uint64(*kandangID) + } else { + updateBody["kandang_id"] = nil } if projectFK != nil { updateBody["project_flock_kandang_id"] = uint64(*projectFK) + } else { + updateBody["project_flock_kandang_id"] = nil } if err := b.db.WithContext(ctx). diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 43c2bdc7..813fbd6f 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "math" + "mime/multipart" "strings" "time" @@ -57,6 +58,7 @@ type purchaseService struct { ApprovalSvc commonSvc.ApprovalService ExpenseBridge PurchaseExpenseBridge FifoSvc commonSvc.FifoService + DocumentSvc commonSvc.DocumentService approvalWorkflow approvalutils.ApprovalWorkflowKey } @@ -76,6 +78,7 @@ func NewPurchaseService( approvalSvc commonSvc.ApprovalService, expenseBridge PurchaseExpenseBridge, fifoSvc commonSvc.FifoService, + documentSvc commonSvc.DocumentService, ) PurchaseService { return &purchaseService{ Log: utils.Log, @@ -89,6 +92,7 @@ func NewPurchaseService( ApprovalSvc: approvalSvc, ExpenseBridge: expenseBridge, FifoSvc: fifoSvc, + DocumentSvc: documentSvc, approvalWorkflow: utils.ApprovalWorkflowPurchase, } } @@ -615,9 +619,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation if err := s.Validate.Struct(req); err != nil { return nil, err } - ctx := c.Context() - action, err := parseApprovalActionInput(req.Action) if err != nil { return nil, err @@ -664,6 +666,30 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation return updated, nil } + if action == entity.ApprovalActionApproved && len(req.TravelDocuments) > 0 { + if len(req.TravelDocuments) > len(req.Items) { + return nil, utils.BadRequest("Travel documents exceed total receiving items") + } + for idx, file := range req.TravelDocuments { + if file == nil { + continue + } + if idx >= len(req.Items) { + break + } + itemID := req.Items[idx].PurchaseItemID + if itemID == 0 { + return nil, utils.BadRequest("Purchase item id is required for travel document upload") + } + uploadedURL, err := s.uploadTravelDocument(ctx, actorID, itemID, file) + if err != nil { + s.Log.Errorf("Failed to upload travel document for item %d: %+v", itemID, err) + return nil, utils.Internal("Failed to upload travel document") + } + req.Items[idx].TravelDocumentPath = &uploadedURL + } + } + itemMap := make(map[uint]*entity.PurchaseItem, len(purchase.Items)) for i := range purchase.Items { itemMap[purchase.Items[i].Id] = &purchase.Items[i] @@ -807,32 +833,20 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation for _, prep := range prepared { item := prep.item - var oldPWID *uint - if item.ProductWarehouseId != nil { - idCopy := uint(*item.ProductWarehouseId) - oldPWID = &idCopy - } - var newPWID *uint - clearPW := false - // 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, - item.ProjectFlockKandangId, - purchase.CreatedBy, - ) - if err != nil { - return err - } - newPWID = &pwID - } else if oldPWID != nil { - newPWID = oldPWID - clearPW = true + // Always ensure PW after receiving so linkage stays stable. + pwID, err := pwRepoTx.EnsureProductWarehouse( + c.Context(), + uint(item.ProductId), + prep.warehouseID, + item.ProjectFlockKandangId, + purchase.CreatedBy, + ) + if err != nil { + return err } + newPWID = &pwID deltaQty := prep.receivedQty - item.TotalQty switch { @@ -857,7 +871,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation VehicleNumber: prep.payload.VehicleNumber, ReceivedQty: &qtyCopy, ProductWarehouseID: newPWID, - ClearProductWarehouse: clearPW, + ClearProductWarehouse: false, } if prep.overrideWarehouse || uint(item.WarehouseId) != prep.warehouseID { @@ -972,6 +986,38 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation return updated, nil } +func (s *purchaseService) uploadTravelDocument( + ctx context.Context, + actorID uint, + itemID uint, + file *multipart.FileHeader, +) (string, error) { + if file == nil { + return "", errors.New("travel document file is required") + } + if s.DocumentSvc == nil { + return "", errors.New("document service not available") + } + + documentFiles := []commonSvc.DocumentFile{{ + File: file, + Type: string(utils.DocumentTypePurchaseTravel), + }} + results, err := s.DocumentSvc.UploadDocuments(ctx, commonSvc.DocumentUploadRequest{ + DocumentableType: string(utils.DocumentableTypePurchaseItem), + DocumentableID: uint64(itemID), + CreatedBy: &actorID, + Files: documentFiles, + }) + if err != nil { + return "", err + } + if len(results) == 0 { + return "", errors.New("upload result is empty") + } + return results[0].URL, nil +} + func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error) { if err := s.Validate.Struct(req); err != nil { return nil, err diff --git a/internal/modules/purchases/validations/purchase.validation.go b/internal/modules/purchases/validations/purchase.validation.go index 1637ccaf..564cc96f 100644 --- a/internal/modules/purchases/validations/purchase.validation.go +++ b/internal/modules/purchases/validations/purchase.validation.go @@ -1,5 +1,7 @@ package validation +import "mime/multipart" + type PurchaseItemPayload struct { WarehouseID uint `json:"warehouse_id" validate:"required,gt=0"` ProductID uint `json:"product_id" validate:"required,gt=0"` @@ -26,7 +28,7 @@ type StaffPurchaseApprovalItem struct { type ApproveStaffPurchaseRequest struct { Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"` - Items []StaffPurchaseApprovalItem `json:"items,omitempty" validate:"omitempty,min=1,dive"` + Items []StaffPurchaseApprovalItem `json:"items" validate:"omitempty,min=1,dive"` Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` } @@ -36,21 +38,22 @@ type ApproveManagerPurchaseRequest struct { } type ReceivePurchaseItemRequest struct { - PurchaseItemID uint `json:"purchase_item_id" validate:"required,gt=0"` - WarehouseID *uint `json:"warehouse_id" validate:"omitempty,gt=0"` - ReceivedDate string `json:"received_date" validate:"required,datetime=2006-01-02"` - ExpeditionVendorID *uint `json:"expedition_vendor_id,omitempty" validate:"omitempty,gt=0"` - TransportPerItem *float64 `json:"transport_per_item,omitempty" validate:"omitempty,gte=0"` - TravelNumber *string `json:"travel_number" validate:"omitempty,max=100"` - TravelDocumentPath *string `json:"travel_document_path" validate:"omitempty,max=255"` - VehicleNumber *string `json:"vehicle_number" validate:"omitempty,max=100"` - ReceivedQty *float64 `json:"received_qty" validate:"omitempty,gte=0"` + PurchaseItemID uint `form:"purchase_item_id" json:"purchase_item_id" validate:"required,gt=0"` + WarehouseID *uint `form:"warehouse_id" json:"warehouse_id" validate:"omitempty,gt=0"` + ReceivedDate string `form:"received_date" json:"received_date" validate:"required,datetime=2006-01-02"` + ExpeditionVendorID *uint `form:"expedition_vendor_id" json:"expedition_vendor_id,omitempty" validate:"omitempty,gt=0"` + TransportPerItem *float64 `form:"transport_per_item" json:"transport_per_item,omitempty" validate:"omitempty,gte=0"` + TravelNumber *string `form:"travel_number" json:"travel_number" validate:"omitempty,max=100"` + TravelDocumentPath *string `form:"travel_document_path" json:"travel_document_path" validate:"omitempty,max=1024"` + VehicleNumber *string `form:"vehicle_number" json:"vehicle_number" validate:"omitempty,max=100"` + ReceivedQty *float64 `form:"received_qty" json:"received_qty" validate:"omitempty,gte=0"` } type ReceivePurchaseRequest struct { - Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"` - Items []ReceivePurchaseItemRequest `json:"items,omitempty" validate:"omitempty,min=1,dive"` - Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` + Action string `form:"action" json:"action" validate:"required,oneof=APPROVED REJECTED"` + Items []ReceivePurchaseItemRequest `form:"items" json:"items" validate:"min=1,dive"` + TravelDocuments []*multipart.FileHeader `form:"travel_documents" json:"-" validate:"omitempty,dive"` + Notes *string `form:"notes" json:"notes,omitempty" validate:"omitempty,max=500"` } type DeletePurchaseItemsRequest struct { diff --git a/internal/modules/repports/route.go b/internal/modules/repports/route.go index 707ef878..83f133af 100644 --- a/internal/modules/repports/route.go +++ b/internal/modules/repports/route.go @@ -18,6 +18,6 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService route.Get("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense) route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing) route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier) - route.Get("/hpp-per-kandang", ctrl.GetHppPerKandang) + route.Get("/hpp-per-kandang", m.RequirePermissions(m.P_ReportHppPerKandangGetAll),ctrl.GetHppPerKandang) } diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 85b0cc91..b7875605 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -411,10 +411,12 @@ const ( DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT" DocumentTypeExpense DocumentType = "EXPENSE_DOCUMENT" DocumentTypeExpenseRealization DocumentType = "EXPENSE_REALIZATION_DOCUMENT" + DocumentTypePurchaseTravel DocumentType = "PURCHASE_TRAVEL_DOCUMENT" DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER" DocumentableTypeExpense DocumentableType = "EXPENSE" DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION" + DocumentableTypePurchaseItem DocumentableType = "PURCHASE_ITEM" ) // ------------------------------------------------------------------- From bc03c469f24d4c0249e22701d3fea2efc8932928 Mon Sep 17 00:00:00 2001 From: ragilap Date: Wed, 31 Dec 2025 04:00:41 +0700 Subject: [PATCH 3/4] feat(BE-278): add delete document s3 --- .../purchases/services/purchase.service.go | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 813fbd6f..31e55b86 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -1097,6 +1097,10 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del return nil, utils.Internal("Failed to delete purchase items") } + if err := s.deletePurchaseItemDocuments(ctx, itemsToDelete); err != nil { + return nil, utils.Internal("Failed to delete purchase documents") + } + if len(itemsToDelete) > 0 { if err := s.notifyExpenseItemsDeleted(ctx, purchase.Id, itemsToDelete); err != nil { s.Log.Errorf("Failed to sync expense deletion for purchase %d: %+v", purchase.Id, err) @@ -1156,6 +1160,10 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error { return utils.Internal("Failed to delete purchase") } + if err := s.deletePurchaseItemDocuments(ctx, itemsToDelete); err != nil { + return utils.Internal("Failed to delete purchase documents") + } + if len(itemsToDelete) > 0 { if err := s.notifyExpenseItemsDeleted(ctx, uint(id), itemsToDelete); err != nil { s.Log.Errorf("Failed to sync expense deletion for purchase %d: %+v", id, err) @@ -1239,6 +1247,21 @@ func (s *purchaseService) notifyExpenseItemsDeleted(ctx context.Context, purchas } +func (s *purchaseService) deletePurchaseItemDocuments(ctx context.Context, items []entity.PurchaseItem) error { + if s.DocumentSvc == nil || len(items) == 0 { + return nil + } + for _, item := range items { + if item.Id == 0 { + continue + } + if err := s.DocumentSvc.DeleteByTarget(ctx, string(utils.DocumentableTypePurchaseItem), uint64(item.Id), true); err != nil { + return err + } + } + return nil +} + func (s *purchaseService) buildStaffAdjustmentPayload( ctx context.Context, purchase *entity.Purchase, From dbaee7313455b8f237bf03f9698d41adb56ef38e Mon Sep 17 00:00:00 2001 From: ragilap Date: Wed, 31 Dec 2025 07:50:13 +0700 Subject: [PATCH 4/4] feat(BE-278): fix error purchase product warehouse --- internal/modules/purchases/services/purchase.service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 31e55b86..7dac0e19 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -1534,5 +1534,5 @@ func (s *purchaseService) ensureProjectFlockNotClosedForPurchase( return utils.Internal("DB not available for project flock validation") } - return commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(ctx, db, pfkIDs) + return commonSvc.EnsureProjectFlockNotClosedByProjectFlockKandangID(ctx, db, pfkIDs) }