feat(BE-278): unrestrict feat warehouse purchase,adding purchase upload document

This commit is contained in:
ragilap
2025-12-31 03:50:58 +07:00
parent 0396aa0255
commit fd5f83ca58
10 changed files with 159 additions and 63 deletions
+1
View File
@@ -44,6 +44,7 @@ const (
P_ReportExpenseGetAll = "lti.repport.expense.list" P_ReportExpenseGetAll = "lti.repport.expense.list"
P_ReportDeliveryGetAll = "lti.repport.delivery.list" P_ReportDeliveryGetAll = "lti.repport.delivery.list"
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list" P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
) )
const ( const (
@@ -214,10 +214,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get active project flocks for location") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get active project flocks for location")
} }
if len(activeProjectFlocks) == 0 { if len(activeProjectFlocks) > 0 {
return fiber.NewError(fiber.StatusBadRequest, "No active project flocks found for this location")
}
projectFlockIDs := make([]uint64, len(activeProjectFlocks)) projectFlockIDs := make([]uint64, len(activeProjectFlocks))
for i, pf := range activeProjectFlocks { for i, pf := range activeProjectFlocks {
projectFlockIDs[i] = uint64(pf.Id) projectFlockIDs[i] = uint64(pf.Id)
@@ -230,6 +227,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
jsonStr := string(projectFlockIdsJSON) jsonStr := string(projectFlockIdsJSON)
projectFlockIdJSON = &jsonStr projectFlockIdJSON = &jsonStr
} }
}
expense = &entity.Expense{ expense = &entity.Expense{
ReferenceNumber: referenceNumber, ReferenceNumber: referenceNumber,
@@ -199,6 +199,31 @@ func (r *ProductWarehouseRepositoryImpl) CleanupEmpty(ctx context.Context, affec
return nil 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). if err := r.DB().WithContext(ctx).
Model(&entity.PurchaseItem{}). Model(&entity.PurchaseItem{}).
Where("product_warehouse_id IN ?", emptyIDs). Where("product_warehouse_id IN ?", emptyIDs).
@@ -1,6 +1,7 @@
package controller package controller
import ( import (
"encoding/json"
"fmt" "fmt"
"math" "math"
"strconv" "strconv"
@@ -86,7 +87,6 @@ func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error {
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
} }
result, err := ctrl.service.CreateOne(c, req) result, err := ctrl.service.CreateOne(c, req)
if err != nil { if err != nil {
return err return err
@@ -161,10 +161,26 @@ func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
} }
req := new(validation.ReceivePurchaseRequest) req := new(validation.ReceivePurchaseRequest)
if err := c.BodyParser(req); err != nil { form, err := c.MultipartForm()
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") 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 = &notes
} }
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) result, err := ctrl.service.ReceiveProducts(c, uint(id), req)
if err != nil { if err != nil {
return err return err
+1
View File
@@ -98,6 +98,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
approvalService, approvalService,
expenseBridge, expenseBridge,
fifoService, fifoService,
documentSvc,
) )
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
@@ -394,9 +394,13 @@ func (b *expenseBridge) OnItemsReceived(c *fiber.Ctx, purchaseID uint, updates [
} }
if kandangID != nil { if kandangID != nil {
updateBody["kandang_id"] = uint64(*kandangID) updateBody["kandang_id"] = uint64(*kandangID)
} else {
updateBody["kandang_id"] = nil
} }
if projectFK != nil { if projectFK != nil {
updateBody["project_flock_kandang_id"] = uint64(*projectFK) updateBody["project_flock_kandang_id"] = uint64(*projectFK)
} else {
updateBody["project_flock_kandang_id"] = nil
} }
if err := b.db.WithContext(ctx). if err := b.db.WithContext(ctx).
@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"math" "math"
"mime/multipart"
"strings" "strings"
"time" "time"
@@ -57,6 +58,7 @@ type purchaseService struct {
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
ExpenseBridge PurchaseExpenseBridge ExpenseBridge PurchaseExpenseBridge
FifoSvc commonSvc.FifoService FifoSvc commonSvc.FifoService
DocumentSvc commonSvc.DocumentService
approvalWorkflow approvalutils.ApprovalWorkflowKey approvalWorkflow approvalutils.ApprovalWorkflowKey
} }
@@ -76,6 +78,7 @@ func NewPurchaseService(
approvalSvc commonSvc.ApprovalService, approvalSvc commonSvc.ApprovalService,
expenseBridge PurchaseExpenseBridge, expenseBridge PurchaseExpenseBridge,
fifoSvc commonSvc.FifoService, fifoSvc commonSvc.FifoService,
documentSvc commonSvc.DocumentService,
) PurchaseService { ) PurchaseService {
return &purchaseService{ return &purchaseService{
Log: utils.Log, Log: utils.Log,
@@ -89,6 +92,7 @@ func NewPurchaseService(
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
ExpenseBridge: expenseBridge, ExpenseBridge: expenseBridge,
FifoSvc: fifoSvc, FifoSvc: fifoSvc,
DocumentSvc: documentSvc,
approvalWorkflow: utils.ApprovalWorkflowPurchase, 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 { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
ctx := c.Context() ctx := c.Context()
action, err := parseApprovalActionInput(req.Action) action, err := parseApprovalActionInput(req.Action)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -664,6 +666,30 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
return updated, nil 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)) itemMap := make(map[uint]*entity.PurchaseItem, len(purchase.Items))
for i := range purchase.Items { for i := range purchase.Items {
itemMap[purchase.Items[i].Id] = &purchase.Items[i] itemMap[purchase.Items[i].Id] = &purchase.Items[i]
@@ -807,17 +833,9 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
for _, prep := range prepared { for _, prep := range prepared {
item := prep.item item := prep.item
var oldPWID *uint
if item.ProductWarehouseId != nil {
idCopy := uint(*item.ProductWarehouseId)
oldPWID = &idCopy
}
var newPWID *uint var newPWID *uint
clearPW := false
// Always ensure PW when qty > 0 so stockable has target. // Always ensure PW after receiving so linkage stays stable.
if prep.receivedQty > 0 {
pwID, err := pwRepoTx.EnsureProductWarehouse( pwID, err := pwRepoTx.EnsureProductWarehouse(
c.Context(), c.Context(),
uint(item.ProductId), uint(item.ProductId),
@@ -829,10 +847,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
return err return err
} }
newPWID = &pwID newPWID = &pwID
} else if oldPWID != nil {
newPWID = oldPWID
clearPW = true
}
deltaQty := prep.receivedQty - item.TotalQty deltaQty := prep.receivedQty - item.TotalQty
switch { switch {
@@ -857,7 +871,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
VehicleNumber: prep.payload.VehicleNumber, VehicleNumber: prep.payload.VehicleNumber,
ReceivedQty: &qtyCopy, ReceivedQty: &qtyCopy,
ProductWarehouseID: newPWID, ProductWarehouseID: newPWID,
ClearProductWarehouse: clearPW, ClearProductWarehouse: false,
} }
if prep.overrideWarehouse || uint(item.WarehouseId) != prep.warehouseID { 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 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) { func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error) {
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
@@ -1,5 +1,7 @@
package validation package validation
import "mime/multipart"
type PurchaseItemPayload struct { type PurchaseItemPayload struct {
WarehouseID uint `json:"warehouse_id" validate:"required,gt=0"` WarehouseID uint `json:"warehouse_id" validate:"required,gt=0"`
ProductID uint `json:"product_id" validate:"required,gt=0"` ProductID uint `json:"product_id" validate:"required,gt=0"`
@@ -26,7 +28,7 @@ type StaffPurchaseApprovalItem struct {
type ApproveStaffPurchaseRequest struct { type ApproveStaffPurchaseRequest struct {
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"` 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"` Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
} }
@@ -36,21 +38,22 @@ type ApproveManagerPurchaseRequest struct {
} }
type ReceivePurchaseItemRequest struct { type ReceivePurchaseItemRequest struct {
PurchaseItemID uint `json:"purchase_item_id" validate:"required,gt=0"` PurchaseItemID uint `form:"purchase_item_id" json:"purchase_item_id" validate:"required,gt=0"`
WarehouseID *uint `json:"warehouse_id" validate:"omitempty,gt=0"` WarehouseID *uint `form:"warehouse_id" json:"warehouse_id" validate:"omitempty,gt=0"`
ReceivedDate string `json:"received_date" validate:"required,datetime=2006-01-02"` ReceivedDate string `form:"received_date" json:"received_date" validate:"required,datetime=2006-01-02"`
ExpeditionVendorID *uint `json:"expedition_vendor_id,omitempty" validate:"omitempty,gt=0"` ExpeditionVendorID *uint `form:"expedition_vendor_id" json:"expedition_vendor_id,omitempty" validate:"omitempty,gt=0"`
TransportPerItem *float64 `json:"transport_per_item,omitempty" validate:"omitempty,gte=0"` TransportPerItem *float64 `form:"transport_per_item" json:"transport_per_item,omitempty" validate:"omitempty,gte=0"`
TravelNumber *string `json:"travel_number" validate:"omitempty,max=100"` TravelNumber *string `form:"travel_number" json:"travel_number" validate:"omitempty,max=100"`
TravelDocumentPath *string `json:"travel_document_path" validate:"omitempty,max=255"` TravelDocumentPath *string `form:"travel_document_path" json:"travel_document_path" validate:"omitempty,max=1024"`
VehicleNumber *string `json:"vehicle_number" validate:"omitempty,max=100"` VehicleNumber *string `form:"vehicle_number" json:"vehicle_number" validate:"omitempty,max=100"`
ReceivedQty *float64 `json:"received_qty" validate:"omitempty,gte=0"` ReceivedQty *float64 `form:"received_qty" json:"received_qty" validate:"omitempty,gte=0"`
} }
type ReceivePurchaseRequest struct { type ReceivePurchaseRequest struct {
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"` Action string `form:"action" json:"action" validate:"required,oneof=APPROVED REJECTED"`
Items []ReceivePurchaseItemRequest `json:"items,omitempty" validate:"omitempty,min=1,dive"` Items []ReceivePurchaseItemRequest `form:"items" json:"items" validate:"min=1,dive"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` TravelDocuments []*multipart.FileHeader `form:"travel_documents" json:"-" validate:"omitempty,dive"`
Notes *string `form:"notes" json:"notes,omitempty" validate:"omitempty,max=500"`
} }
type DeletePurchaseItemsRequest struct { type DeletePurchaseItemsRequest struct {
+1 -1
View File
@@ -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("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense)
route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing) route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing)
route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier) 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)
} }
+2
View File
@@ -411,10 +411,12 @@ const (
DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT" DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT"
DocumentTypeExpense DocumentType = "EXPENSE_DOCUMENT" DocumentTypeExpense DocumentType = "EXPENSE_DOCUMENT"
DocumentTypeExpenseRealization DocumentType = "EXPENSE_REALIZATION_DOCUMENT" DocumentTypeExpenseRealization DocumentType = "EXPENSE_REALIZATION_DOCUMENT"
DocumentTypePurchaseTravel DocumentType = "PURCHASE_TRAVEL_DOCUMENT"
DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER" DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER"
DocumentableTypeExpense DocumentableType = "EXPENSE" DocumentableTypeExpense DocumentableType = "EXPENSE"
DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION" DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION"
DocumentableTypePurchaseItem DocumentableType = "PURCHASE_ITEM"
) )
// ------------------------------------------------------------------- // -------------------------------------------------------------------