feat(BE-229,234,235,230,231,232,233): purchase request and purchase order and fix master data dto

This commit is contained in:
ragilap
2025-11-17 11:53:27 +07:00
parent 11f2389ec5
commit 0708628b78
2 changed files with 27 additions and 56 deletions
@@ -355,7 +355,6 @@ func (s *purchaseService) processStaffPurchaseApproval(c *fiber.Ctx, id uint64,
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase is not ready for staff approval")
}
// Detect if purchase already has any receiving data on its items.
hasReceivingData := false
for _, item := range purchase.Items {
if item.TotalQty > 0 || item.TotalUsed > 0 {
@@ -364,8 +363,6 @@ func (s *purchaseService) processStaffPurchaseApproval(c *fiber.Ctx, id uint64,
}
}
// After there is receiving data, staff edits are allowed to change quantity
// in sync with receiving, without creating new receiving approvals here.
syncReceiving := !isInitialApproval && hasReceivingData
payload, err := s.buildStaffAdjustmentPayload(ctx, purchase, req, syncReceiving)
@@ -611,6 +608,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint64, req *validati
receivedQty float64
}
visitedItems := make(map[uint64]struct{}, len(req.Items))
prepared := make([]preparedReceiving, 0, len(req.Items))
for _, payload := range req.Items {
item, exists := itemMap[payload.PurchaseItemID]
@@ -647,6 +645,11 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint64, req *validati
return nil, fiber.NewError(fiber.StatusBadRequest, 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))
}
visitedItems[payload.PurchaseItemID] = struct{}{}
prepared = append(prepared, preparedReceiving{
item: item,
payload: payload,
@@ -657,6 +660,12 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint64, req *validati
})
}
// 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")
}
receivingAction := entity.ApprovalActionApproved
completedAction := entity.ApprovalActionApproved
actorID := uint(1)
@@ -1085,57 +1094,21 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
return nil, fiber.NewError(fiber.StatusBadRequest, "No available warehouses for this purchase")
}
productSupplierCache := make(map[uint64]bool)
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))
}
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),
)
}
if data.WarehouseID != 0 && data.WarehouseID != item.WarehouseId {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse mismatch for item %d", item.Id))
}
effectiveProductID := item.ProductId
if data.ProductID != 0 && data.ProductID != item.ProductId {
if item.TotalQty > 0 || item.TotalUsed > 0 {
return nil, fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Cannot change product for item %d because it already has receiving data", item.Id),
)
}
if _, checked := productSupplierCache[data.ProductID]; !checked {
linked, err := s.ProductRepo.IsLinkedToSupplier(ctx, uint(data.ProductID), uint(purchase.SupplierId))
if err != nil {
s.Log.Errorf("Failed to validate product %d for supplier %d: %+v", data.ProductID, purchase.SupplierId, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product for supplier")
}
if !linked {
return nil, fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Product %d is not linked to supplier %d", data.ProductID, purchase.SupplierId),
)
}
productSupplierCache[data.ProductID] = true
}
oldKey := fmt.Sprintf("%d:%d", item.ProductId, item.WarehouseId)
newKey := fmt.Sprintf("%d:%d", data.ProductID, item.WarehouseId)
if _, exists := existingCombos[newKey]; exists && newKey != oldKey {
return nil, fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Product %d in warehouse %d already exists in this purchase", data.ProductID, item.WarehouseId),
)
}
if newKey != oldKey {
delete(existingCombos, oldKey)
existingCombos[newKey] = struct{}{}
}
effectiveProductID = data.ProductID
}
effectiveQty := item.SubQty
if data.Qty != nil {
if *data.Qty <= 0 {
@@ -1161,10 +1134,6 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
Price: data.Price,
TotalPrice: totalPrice,
}
if effectiveProductID != item.ProductId {
productIDCopy := effectiveProductID
update.ProductID = &productIDCopy
}
if data.Qty != nil {
qtyCopy := effectiveQty
update.Quantity = &qtyCopy
@@ -1182,6 +1151,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
return nil, fiber.NewError(fiber.StatusBadRequest, "Found pricing data for items that do not belong to this purchase")
}
productSupplierCache := make(map[uint64]bool)
newItems := make([]*entity.PurchaseItem, 0, len(newPayloads))
for _, payload := range newPayloads {
@@ -3,7 +3,7 @@ package validation
type PurchaseItemPayload struct {
WarehouseID uint `json:"warehouse_id" validate:"required,gt=0"`
ProductID uint `json:"product_id" validate:"required,gt=0"`
Quantity float64 `json:"quantity" validate:"required,gt=0"`
Quantity float64 `json:"qty" validate:"required,gt=0"`
}
type CreatePurchaseRequest struct {
@@ -14,12 +14,13 @@ type CreatePurchaseRequest struct {
}
type StaffPurchaseApprovalItem struct {
PurchaseItemID uint64 `json:"purchase_item_id,omitempty" validate:"omitempty,gt=0"`
ProductID uint64 `json:"product_id" validate:"required,gt=0"`
WarehouseID uint64 `json:"warehouse_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
Qty *float64 `json:"qty,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
Price float64 `json:"price" validate:"required,gt=0"`
TotalPrice float64 `json:"total_price" validate:"required,gt=0"`
PurchaseItemID uint64 `json:"purchase_item_id,omitempty" validate:"omitempty,gt=0"`
// For new items (no purchase_item_id), product_id is required.
ProductID uint64 `json:"product_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
WarehouseID uint64 `json:"warehouse_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
Qty *float64 `json:"qty,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
Price float64 `json:"price" validate:"required,gt=0"`
TotalPrice float64 `json:"total_price" validate:"required,gt=0"`
}
type ApproveStaffPurchaseRequest struct {