mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 15:25:43 +00:00
feat(BE-229,234,235,230,231,232,233): purchase request and purchase order and fix master data dto
This commit is contained in:
@@ -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")
|
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
|
hasReceivingData := false
|
||||||
for _, item := range purchase.Items {
|
for _, item := range purchase.Items {
|
||||||
if item.TotalQty > 0 || item.TotalUsed > 0 {
|
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
|
syncReceiving := !isInitialApproval && hasReceivingData
|
||||||
|
|
||||||
payload, err := s.buildStaffAdjustmentPayload(ctx, purchase, req, syncReceiving)
|
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
|
receivedQty float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
visitedItems := make(map[uint64]struct{}, len(req.Items))
|
||||||
prepared := make([]preparedReceiving, 0, len(req.Items))
|
prepared := make([]preparedReceiving, 0, len(req.Items))
|
||||||
for _, payload := range req.Items {
|
for _, payload := range req.Items {
|
||||||
item, exists := itemMap[payload.PurchaseItemID]
|
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))
|
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{
|
prepared = append(prepared, preparedReceiving{
|
||||||
item: item,
|
item: item,
|
||||||
payload: payload,
|
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
|
receivingAction := entity.ApprovalActionApproved
|
||||||
completedAction := entity.ApprovalActionApproved
|
completedAction := entity.ApprovalActionApproved
|
||||||
actorID := uint(1)
|
actorID := uint(1)
|
||||||
@@ -1085,57 +1094,21 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "No available warehouses for this purchase")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "No available warehouses for this purchase")
|
||||||
}
|
}
|
||||||
|
|
||||||
productSupplierCache := make(map[uint64]bool)
|
|
||||||
|
|
||||||
for _, item := range purchase.Items {
|
for _, item := range purchase.Items {
|
||||||
data, ok := requestItems[item.Id]
|
data, ok := requestItems[item.Id]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Missing pricing data for item %d", item.Id))
|
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 {
|
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, 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
|
effectiveQty := item.SubQty
|
||||||
if data.Qty != nil {
|
if data.Qty != nil {
|
||||||
if *data.Qty <= 0 {
|
if *data.Qty <= 0 {
|
||||||
@@ -1161,10 +1134,6 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|||||||
Price: data.Price,
|
Price: data.Price,
|
||||||
TotalPrice: totalPrice,
|
TotalPrice: totalPrice,
|
||||||
}
|
}
|
||||||
if effectiveProductID != item.ProductId {
|
|
||||||
productIDCopy := effectiveProductID
|
|
||||||
update.ProductID = &productIDCopy
|
|
||||||
}
|
|
||||||
if data.Qty != nil {
|
if data.Qty != nil {
|
||||||
qtyCopy := effectiveQty
|
qtyCopy := effectiveQty
|
||||||
update.Quantity = &qtyCopy
|
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")
|
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))
|
newItems := make([]*entity.PurchaseItem, 0, len(newPayloads))
|
||||||
|
|
||||||
for _, payload := range newPayloads {
|
for _, payload := range newPayloads {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package validation
|
|||||||
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"`
|
||||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
Quantity float64 `json:"qty" validate:"required,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreatePurchaseRequest struct {
|
type CreatePurchaseRequest struct {
|
||||||
@@ -14,12 +14,13 @@ type CreatePurchaseRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StaffPurchaseApprovalItem struct {
|
type StaffPurchaseApprovalItem struct {
|
||||||
PurchaseItemID uint64 `json:"purchase_item_id,omitempty" validate:"omitempty,gt=0"`
|
PurchaseItemID uint64 `json:"purchase_item_id,omitempty" validate:"omitempty,gt=0"`
|
||||||
ProductID uint64 `json:"product_id" validate:"required,gt=0"`
|
// For new items (no purchase_item_id), product_id is required.
|
||||||
WarehouseID uint64 `json:"warehouse_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
|
ProductID uint64 `json:"product_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
|
||||||
Qty *float64 `json:"qty,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
|
WarehouseID uint64 `json:"warehouse_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
|
||||||
Price float64 `json:"price" validate:"required,gt=0"`
|
Qty *float64 `json:"qty,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
|
||||||
TotalPrice float64 `json:"total_price" validate:"required,gt=0"`
|
Price float64 `json:"price" validate:"required,gt=0"`
|
||||||
|
TotalPrice float64 `json:"total_price" validate:"required,gt=0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApproveStaffPurchaseRequest struct {
|
type ApproveStaffPurchaseRequest struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user