Add Inventory Report
This commit is contained in:
parent
bb9a81c7c1
commit
7adba2c8f5
@ -77,6 +77,8 @@ type InventoryReportSummaryResponse struct {
|
|||||||
LowStockIngredients int `json:"low_stock_ingredients"`
|
LowStockIngredients int `json:"low_stock_ingredients"`
|
||||||
ZeroStockProducts int `json:"zero_stock_products"`
|
ZeroStockProducts int `json:"zero_stock_products"`
|
||||||
ZeroStockIngredients int `json:"zero_stock_ingredients"`
|
ZeroStockIngredients int `json:"zero_stock_ingredients"`
|
||||||
|
TotalSoldProducts float64 `json:"total_sold_products"`
|
||||||
|
TotalSoldIngredients float64 `json:"total_sold_ingredients"`
|
||||||
OutletID string `json:"outlet_id"`
|
OutletID string `json:"outlet_id"`
|
||||||
OutletName string `json:"outlet_name"`
|
OutletName string `json:"outlet_name"`
|
||||||
GeneratedAt string `json:"generated_at"`
|
GeneratedAt string `json:"generated_at"`
|
||||||
@ -97,6 +99,7 @@ type InventoryProductDetailResponse struct {
|
|||||||
ReorderLevel int `json:"reorder_level"`
|
ReorderLevel int `json:"reorder_level"`
|
||||||
UnitCost float64 `json:"unit_cost"`
|
UnitCost float64 `json:"unit_cost"`
|
||||||
TotalValue float64 `json:"total_value"`
|
TotalValue float64 `json:"total_value"`
|
||||||
|
TotalSold float64 `json:"total_sold"`
|
||||||
IsLowStock bool `json:"is_low_stock"`
|
IsLowStock bool `json:"is_low_stock"`
|
||||||
IsZeroStock bool `json:"is_zero_stock"`
|
IsZeroStock bool `json:"is_zero_stock"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
@ -111,6 +114,7 @@ type InventoryIngredientDetailResponse struct {
|
|||||||
ReorderLevel int `json:"reorder_level"`
|
ReorderLevel int `json:"reorder_level"`
|
||||||
UnitCost float64 `json:"unit_cost"`
|
UnitCost float64 `json:"unit_cost"`
|
||||||
TotalValue float64 `json:"total_value"`
|
TotalValue float64 `json:"total_value"`
|
||||||
|
TotalSold float64 `json:"total_sold"`
|
||||||
IsLowStock bool `json:"is_low_stock"`
|
IsLowStock bool `json:"is_low_stock"`
|
||||||
IsZeroStock bool `json:"is_zero_stock"`
|
IsZeroStock bool `json:"is_zero_stock"`
|
||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"apskel-pos-be/internal/appcontext"
|
"apskel-pos-be/internal/appcontext"
|
||||||
"apskel-pos-be/internal/constants"
|
"apskel-pos-be/internal/constants"
|
||||||
@ -279,7 +280,6 @@ func (h *InventoryHandler) GetZeroStockItems(c *gin.Context) {
|
|||||||
util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::GetZeroStockItems")
|
util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::GetZeroStockItems")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInventoryReportSummary returns summary statistics for inventory report
|
|
||||||
func (h *InventoryHandler) GetInventoryReportSummary(c *gin.Context) {
|
func (h *InventoryHandler) GetInventoryReportSummary(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
contextInfo := appcontext.FromGinContext(ctx)
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
@ -293,7 +293,20 @@ func (h *InventoryHandler) GetInventoryReportSummary(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err := h.inventoryService.GetInventoryReportSummary(ctx, outletID, contextInfo.OrganizationID)
|
// Parse date range parameters for summary
|
||||||
|
var dateFrom, dateTo *time.Time
|
||||||
|
if dateFromStr := c.Query("date_from"); dateFromStr != "" {
|
||||||
|
if parsedDateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil {
|
||||||
|
dateFrom = &parsedDateFrom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dateToStr := c.Query("date_to"); dateToStr != "" {
|
||||||
|
if parsedDateTo, err := time.Parse("2006-01-02", dateToStr); err == nil {
|
||||||
|
dateTo = &parsedDateTo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
summary, err := h.inventoryService.GetInventoryReportSummary(ctx, outletID, contextInfo.OrganizationID, dateFrom, dateTo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportSummary -> Failed to get inventory report summary from service")
|
logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportSummary -> Failed to get inventory report summary from service")
|
||||||
responseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
responseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error())
|
||||||
@ -305,16 +318,13 @@ func (h *InventoryHandler) GetInventoryReportSummary(c *gin.Context) {
|
|||||||
util.HandleResponse(c.Writer, c.Request, response, "InventoryHandler::GetInventoryReportSummary")
|
util.HandleResponse(c.Writer, c.Request, response, "InventoryHandler::GetInventoryReportSummary")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInventoryReportDetails returns detailed inventory report with products and ingredients
|
|
||||||
func (h *InventoryHandler) GetInventoryReportDetails(c *gin.Context) {
|
func (h *InventoryHandler) GetInventoryReportDetails(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
contextInfo := appcontext.FromGinContext(ctx)
|
contextInfo := appcontext.FromGinContext(ctx)
|
||||||
|
|
||||||
// Parse query parameters
|
|
||||||
filter := &models.InventoryReportFilter{}
|
filter := &models.InventoryReportFilter{}
|
||||||
|
|
||||||
// Parse outlet_id (required)
|
if outletIDStr := c.Param("outlet_id"); outletIDStr != "" {
|
||||||
if outletIDStr := c.Query("outlet_id"); outletIDStr != "" {
|
|
||||||
outletID, err := uuid.Parse(outletIDStr)
|
outletID, err := uuid.Parse(outletIDStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportDetails -> Invalid outlet ID")
|
logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportDetails -> Invalid outlet ID")
|
||||||
@ -330,7 +340,6 @@ func (h *InventoryHandler) GetInventoryReportDetails(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse category_id (optional)
|
|
||||||
if categoryIDStr := c.Query("category_id"); categoryIDStr != "" {
|
if categoryIDStr := c.Query("category_id"); categoryIDStr != "" {
|
||||||
categoryID, err := uuid.Parse(categoryIDStr)
|
categoryID, err := uuid.Parse(categoryIDStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -375,6 +384,18 @@ func (h *InventoryHandler) GetInventoryReportDetails(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dateFromStr := c.Query("date_from")
|
||||||
|
dateToStr := c.Query("date_to")
|
||||||
|
|
||||||
|
if fromTime, toTime, err := util.ParseDateRangeToJakartaTime(dateFromStr, dateToStr); err == nil {
|
||||||
|
if fromTime != nil {
|
||||||
|
filter.DateFrom = fromTime
|
||||||
|
}
|
||||||
|
if toTime != nil {
|
||||||
|
filter.DateTo = toTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
report, err := h.inventoryService.GetInventoryReportDetails(ctx, filter, contextInfo.OrganizationID)
|
report, err := h.inventoryService.GetInventoryReportDetails(ctx, filter, contextInfo.OrganizationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportDetails -> Failed to get inventory report details from service")
|
logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportDetails -> Failed to get inventory report details from service")
|
||||||
|
|||||||
@ -70,6 +70,8 @@ type InventoryReportSummary struct {
|
|||||||
LowStockIngredients int `json:"low_stock_ingredients"`
|
LowStockIngredients int `json:"low_stock_ingredients"`
|
||||||
ZeroStockProducts int `json:"zero_stock_products"`
|
ZeroStockProducts int `json:"zero_stock_products"`
|
||||||
ZeroStockIngredients int `json:"zero_stock_ingredients"`
|
ZeroStockIngredients int `json:"zero_stock_ingredients"`
|
||||||
|
TotalSoldProducts float64 `json:"total_sold_products"`
|
||||||
|
TotalSoldIngredients float64 `json:"total_sold_ingredients"`
|
||||||
OutletID uuid.UUID `json:"outlet_id"`
|
OutletID uuid.UUID `json:"outlet_id"`
|
||||||
OutletName string `json:"outlet_name"`
|
OutletName string `json:"outlet_name"`
|
||||||
GeneratedAt time.Time `json:"generated_at"`
|
GeneratedAt time.Time `json:"generated_at"`
|
||||||
@ -90,6 +92,7 @@ type InventoryProductDetail struct {
|
|||||||
ReorderLevel int `json:"reorder_level"`
|
ReorderLevel int `json:"reorder_level"`
|
||||||
UnitCost float64 `json:"unit_cost"`
|
UnitCost float64 `json:"unit_cost"`
|
||||||
TotalValue float64 `json:"total_value"`
|
TotalValue float64 `json:"total_value"`
|
||||||
|
TotalSold float64 `json:"total_sold"`
|
||||||
IsLowStock bool `json:"is_low_stock"`
|
IsLowStock bool `json:"is_low_stock"`
|
||||||
IsZeroStock bool `json:"is_zero_stock"`
|
IsZeroStock bool `json:"is_zero_stock"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@ -104,6 +107,7 @@ type InventoryIngredientDetail struct {
|
|||||||
ReorderLevel int `json:"reorder_level"`
|
ReorderLevel int `json:"reorder_level"`
|
||||||
UnitCost float64 `json:"unit_cost"`
|
UnitCost float64 `json:"unit_cost"`
|
||||||
TotalValue float64 `json:"total_value"`
|
TotalValue float64 `json:"total_value"`
|
||||||
|
TotalSold float64 `json:"total_sold"`
|
||||||
IsLowStock bool `json:"is_low_stock"`
|
IsLowStock bool `json:"is_low_stock"`
|
||||||
IsZeroStock bool `json:"is_zero_stock"`
|
IsZeroStock bool `json:"is_zero_stock"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@ -117,4 +121,6 @@ type InventoryReportFilter struct {
|
|||||||
Search *string `json:"search"`
|
Search *string `json:"search"`
|
||||||
Limit *int `json:"limit"`
|
Limit *int `json:"limit"`
|
||||||
Offset *int `json:"offset"`
|
Offset *int `json:"offset"`
|
||||||
|
DateFrom *time.Time `json:"date_from"`
|
||||||
|
DateTo *time.Time `json:"date_to"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package processor
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"apskel-pos-be/internal/mappers"
|
"apskel-pos-be/internal/mappers"
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
@ -25,7 +26,7 @@ type InventoryProcessor interface {
|
|||||||
AdjustQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, delta int) (*models.InventoryResponse, error)
|
AdjustQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, delta int) (*models.InventoryResponse, error)
|
||||||
SetQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, quantity int) (*models.InventoryResponse, error)
|
SetQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, quantity int) (*models.InventoryResponse, error)
|
||||||
UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int, organizationID uuid.UUID) error
|
UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int, organizationID uuid.UUID) error
|
||||||
GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID) (*models.InventoryReportSummary, error)
|
GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID, dateFrom, dateTo *time.Time) (*models.InventoryReportSummary, error)
|
||||||
GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*models.InventoryReportDetail, error)
|
GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*models.InventoryReportDetail, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,9 +258,7 @@ func (p *InventoryProcessorImpl) GetZeroStock(ctx context.Context, outletID, org
|
|||||||
return responses, nil
|
return responses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetInventoryReportSummary returns summary statistics for inventory report
|
func (p *InventoryProcessorImpl) GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID, dateFrom, dateTo *time.Time) (*models.InventoryReportSummary, error) {
|
||||||
func (p *InventoryProcessorImpl) GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID) (*models.InventoryReportSummary, error) {
|
|
||||||
// Verify outlet belongs to organization
|
|
||||||
outlet, err := p.outletRepo.GetByID(ctx, outletID)
|
outlet, err := p.outletRepo.GetByID(ctx, outletID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("outlet not found: %w", err)
|
return nil, fmt.Errorf("outlet not found: %w", err)
|
||||||
@ -268,7 +267,7 @@ func (p *InventoryProcessorImpl) GetInventoryReportSummary(ctx context.Context,
|
|||||||
return nil, fmt.Errorf("outlet does not belong to the organization")
|
return nil, fmt.Errorf("outlet does not belong to the organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
summary, err := p.inventoryRepo.GetInventoryReportSummary(ctx, outletID)
|
summary, err := p.inventoryRepo.GetInventoryReportSummary(ctx, outletID, dateFrom, dateTo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get inventory report summary: %w", err)
|
return nil, fmt.Errorf("failed to get inventory report summary: %w", err)
|
||||||
}
|
}
|
||||||
@ -282,7 +281,6 @@ func (p *InventoryProcessorImpl) GetInventoryReportDetails(ctx context.Context,
|
|||||||
return nil, fmt.Errorf("outlet_id is required for inventory report")
|
return nil, fmt.Errorf("outlet_id is required for inventory report")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify outlet belongs to organization
|
|
||||||
outlet, err := p.outletRepo.GetByID(ctx, *filter.OutletID)
|
outlet, err := p.outletRepo.GetByID(ctx, *filter.OutletID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("outlet not found: %w", err)
|
return nil, fmt.Errorf("outlet not found: %w", err)
|
||||||
|
|||||||
@ -1334,18 +1334,8 @@ type ingredientRecipeData struct {
|
|||||||
|
|
||||||
// prepareIngredientRecipeData prepares ingredient recipe data without making database calls
|
// prepareIngredientRecipeData prepares ingredient recipe data without making database calls
|
||||||
func (p *OrderProcessorImpl) prepareIngredientRecipeData(ctx context.Context, item *entities.OrderItem, order *entities.Order, payment *entities.Payment) (*ingredientRecipeData, error) {
|
func (p *OrderProcessorImpl) prepareIngredientRecipeData(ctx context.Context, item *entities.OrderItem, order *entities.Order, payment *entities.Payment) (*ingredientRecipeData, error) {
|
||||||
// Check if the product has ingredients
|
|
||||||
product, err := p.productRepo.GetByID(ctx, item.ProductID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get product: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !product.HasIngredients {
|
|
||||||
return &ingredientRecipeData{}, nil // Product doesn't have ingredients
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get product recipes based on variant (if any)
|
|
||||||
var recipes []*entities.ProductRecipe
|
var recipes []*entities.ProductRecipe
|
||||||
|
var err error
|
||||||
if item.ProductVariantID != nil {
|
if item.ProductVariantID != nil {
|
||||||
recipes, err = p.productRecipeRepo.GetByProductAndVariantID(ctx, item.ProductID, item.ProductVariantID, order.OrganizationID)
|
recipes, err = p.productRecipeRepo.GetByProductAndVariantID(ctx, item.ProductID, item.ProductVariantID, order.OrganizationID)
|
||||||
} else {
|
} else {
|
||||||
@ -1363,7 +1353,6 @@ func (p *OrderProcessorImpl) prepareIngredientRecipeData(ctx context.Context, it
|
|||||||
var ingredientUpdates []*entities.Ingredient
|
var ingredientUpdates []*entities.Ingredient
|
||||||
var movements []*entities.InventoryMovement
|
var movements []*entities.InventoryMovement
|
||||||
|
|
||||||
// Process each ingredient in the recipe
|
|
||||||
for _, recipe := range recipes {
|
for _, recipe := range recipes {
|
||||||
ingredientData, err := p.prepareIngredientRecipeItem(ctx, recipe, item, order, payment)
|
ingredientData, err := p.prepareIngredientRecipeItem(ctx, recipe, item, order, payment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1388,20 +1377,15 @@ type ingredientRecipeItem struct {
|
|||||||
|
|
||||||
// prepareIngredientRecipeItem prepares data for a single ingredient recipe without making database calls
|
// prepareIngredientRecipeItem prepares data for a single ingredient recipe without making database calls
|
||||||
func (p *OrderProcessorImpl) prepareIngredientRecipeItem(ctx context.Context, recipe *entities.ProductRecipe, item *entities.OrderItem, order *entities.Order, payment *entities.Payment) (*ingredientRecipeItem, error) {
|
func (p *OrderProcessorImpl) prepareIngredientRecipeItem(ctx context.Context, recipe *entities.ProductRecipe, item *entities.OrderItem, order *entities.Order, payment *entities.Payment) (*ingredientRecipeItem, error) {
|
||||||
// Calculate total ingredient quantity needed
|
|
||||||
totalIngredientQuantity := recipe.Quantity * float64(item.Quantity)
|
totalIngredientQuantity := recipe.Quantity * float64(item.Quantity)
|
||||||
|
|
||||||
// Get current ingredient details
|
|
||||||
currentIngredient, err := p.ingredientRepo.GetByID(ctx, recipe.IngredientID, order.OrganizationID)
|
currentIngredient, err := p.ingredientRepo.GetByID(ctx, recipe.IngredientID, order.OrganizationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get ingredient: %w", err)
|
return nil, fmt.Errorf("failed to get ingredient: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For ingredients, we typically don't track quantity in the ingredient entity itself
|
currentIngredient.Stock -= totalIngredientQuantity
|
||||||
// Instead, we create inventory movement records to track consumption
|
|
||||||
// The ingredient entity remains unchanged, but we track the movement
|
|
||||||
|
|
||||||
// Prepare movement record
|
|
||||||
movement := &entities.InventoryMovement{
|
movement := &entities.InventoryMovement{
|
||||||
OrganizationID: order.OrganizationID,
|
OrganizationID: order.OrganizationID,
|
||||||
OutletID: order.OutletID,
|
OutletID: order.OutletID,
|
||||||
@ -1409,8 +1393,8 @@ func (p *OrderProcessorImpl) prepareIngredientRecipeItem(ctx context.Context, re
|
|||||||
ItemType: "INGREDIENT",
|
ItemType: "INGREDIENT",
|
||||||
MovementType: entities.InventoryMovementTypeIngredient,
|
MovementType: entities.InventoryMovementTypeIngredient,
|
||||||
Quantity: -totalIngredientQuantity,
|
Quantity: -totalIngredientQuantity,
|
||||||
PreviousQuantity: 0, // We don't track current quantity in ingredient entity
|
PreviousQuantity: 0,
|
||||||
NewQuantity: 0, // We don't track current quantity in ingredient entity
|
NewQuantity: 0,
|
||||||
UnitCost: currentIngredient.Cost,
|
UnitCost: currentIngredient.Cost,
|
||||||
TotalCost: totalIngredientQuantity * currentIngredient.Cost,
|
TotalCost: totalIngredientQuantity * currentIngredient.Cost,
|
||||||
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
ReferenceType: func() *entities.InventoryMovementReferenceType {
|
||||||
|
|||||||
@ -33,7 +33,7 @@ type InventoryRepository interface {
|
|||||||
BulkUpdate(ctx context.Context, inventoryItems []*entities.Inventory) error
|
BulkUpdate(ctx context.Context, inventoryItems []*entities.Inventory) error
|
||||||
BulkAdjustQuantity(ctx context.Context, adjustments map[uuid.UUID]int, outletID uuid.UUID) error
|
BulkAdjustQuantity(ctx context.Context, adjustments map[uuid.UUID]int, outletID uuid.UUID) error
|
||||||
GetTotalValueByOutlet(ctx context.Context, outletID uuid.UUID) (float64, error)
|
GetTotalValueByOutlet(ctx context.Context, outletID uuid.UUID) (float64, error)
|
||||||
GetInventoryReportSummary(ctx context.Context, outletID uuid.UUID) (*models.InventoryReportSummary, error)
|
GetInventoryReportSummary(ctx context.Context, outletID uuid.UUID, dateFrom, dateTo *time.Time) (*models.InventoryReportSummary, error)
|
||||||
GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter) (*models.InventoryReportDetail, error)
|
GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter) (*models.InventoryReportDetail, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,7 +343,7 @@ func (r *InventoryRepositoryImpl) GetTotalValueByOutlet(ctx context.Context, out
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetInventoryReportSummary returns summary statistics for inventory report
|
// GetInventoryReportSummary returns summary statistics for inventory report
|
||||||
func (r *InventoryRepositoryImpl) GetInventoryReportSummary(ctx context.Context, outletID uuid.UUID) (*models.InventoryReportSummary, error) {
|
func (r *InventoryRepositoryImpl) GetInventoryReportSummary(ctx context.Context, outletID uuid.UUID, dateFrom, dateTo *time.Time) (*models.InventoryReportSummary, error) {
|
||||||
var summary models.InventoryReportSummary
|
var summary models.InventoryReportSummary
|
||||||
summary.OutletID = outletID
|
summary.OutletID = outletID
|
||||||
summary.GeneratedAt = time.Now()
|
summary.GeneratedAt = time.Now()
|
||||||
@ -355,37 +355,32 @@ func (r *InventoryRepositoryImpl) GetInventoryReportSummary(ctx context.Context,
|
|||||||
}
|
}
|
||||||
summary.OutletName = outlet.Name
|
summary.OutletName = outlet.Name
|
||||||
|
|
||||||
// Get total products count
|
|
||||||
var totalProducts int64
|
var totalProducts int64
|
||||||
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
|
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
|
||||||
Joins("JOIN products ON inventory.product_id = products.id").
|
Joins("JOIN products ON inventory.product_id = products.id").
|
||||||
Where("inventory.outlet_id = ? AND products.has_ingredients = false", outletID).
|
Where("inventory.outlet_id = ?", outletID).
|
||||||
Count(&totalProducts).Error; err != nil {
|
Count(&totalProducts).Error; err != nil {
|
||||||
return nil, fmt.Errorf("failed to count total products: %w", err)
|
return nil, fmt.Errorf("failed to count total products: %w", err)
|
||||||
}
|
}
|
||||||
summary.TotalProducts = int(totalProducts)
|
summary.TotalProducts = int(totalProducts)
|
||||||
|
|
||||||
// Get total ingredients count
|
|
||||||
var totalIngredients int64
|
var totalIngredients int64
|
||||||
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
|
if err := r.db.WithContext(ctx).Model(&entities.Ingredient{}).
|
||||||
Joins("JOIN ingredients ON inventory.product_id = ingredients.id").
|
Where("outlet_id = ? AND is_active = ?", outletID, true).
|
||||||
Where("inventory.outlet_id = ?", outletID).
|
|
||||||
Count(&totalIngredients).Error; err != nil {
|
Count(&totalIngredients).Error; err != nil {
|
||||||
return nil, fmt.Errorf("failed to count total ingredients: %w", err)
|
return nil, fmt.Errorf("failed to count total ingredients: %w", err)
|
||||||
}
|
}
|
||||||
summary.TotalIngredients = int(totalIngredients)
|
summary.TotalIngredients = int(totalIngredients)
|
||||||
|
|
||||||
// Get low stock products count
|
|
||||||
var lowStockProducts int64
|
var lowStockProducts int64
|
||||||
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
|
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
|
||||||
Joins("JOIN products ON inventory.product_id = products.id").
|
Joins("JOIN products ON inventory.product_id = products.id").
|
||||||
Where("inventory.outlet_id = ? AND products.has_ingredients = false AND inventory.quantity <= inventory.reorder_level AND inventory.quantity > 0", outletID).
|
Where("inventory.outlet_id = ? AND inventory.quantity <= inventory.reorder_level AND inventory.quantity > 0", outletID).
|
||||||
Count(&lowStockProducts).Error; err != nil {
|
Count(&lowStockProducts).Error; err != nil {
|
||||||
return nil, fmt.Errorf("failed to count low stock products: %w", err)
|
return nil, fmt.Errorf("failed to count low stock products: %w", err)
|
||||||
}
|
}
|
||||||
summary.LowStockProducts = int(lowStockProducts)
|
summary.LowStockProducts = int(lowStockProducts)
|
||||||
|
|
||||||
// Get low stock ingredients count
|
|
||||||
var lowStockIngredients int64
|
var lowStockIngredients int64
|
||||||
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
|
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
|
||||||
Joins("JOIN ingredients ON inventory.product_id = ingredients.id").
|
Joins("JOIN ingredients ON inventory.product_id = ingredients.id").
|
||||||
@ -395,27 +390,31 @@ func (r *InventoryRepositoryImpl) GetInventoryReportSummary(ctx context.Context,
|
|||||||
}
|
}
|
||||||
summary.LowStockIngredients = int(lowStockIngredients)
|
summary.LowStockIngredients = int(lowStockIngredients)
|
||||||
|
|
||||||
// Get zero stock products count
|
|
||||||
var zeroStockProducts int64
|
var zeroStockProducts int64
|
||||||
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
|
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
|
||||||
Joins("JOIN products ON inventory.product_id = products.id").
|
Joins("JOIN products ON inventory.product_id = products.id").
|
||||||
Where("inventory.outlet_id = ? AND products.has_ingredients = false AND inventory.quantity = 0", outletID).
|
Where("inventory.outlet_id = ? AND inventory.quantity = 0", outletID).
|
||||||
Count(&zeroStockProducts).Error; err != nil {
|
Count(&zeroStockProducts).Error; err != nil {
|
||||||
return nil, fmt.Errorf("failed to count zero stock products: %w", err)
|
return nil, fmt.Errorf("failed to count zero stock products: %w", err)
|
||||||
}
|
}
|
||||||
summary.ZeroStockProducts = int(zeroStockProducts)
|
summary.ZeroStockProducts = int(zeroStockProducts)
|
||||||
|
|
||||||
// Get zero stock ingredients count
|
|
||||||
var zeroStockIngredients int64
|
var zeroStockIngredients int64
|
||||||
if err := r.db.WithContext(ctx).Model(&entities.Inventory{}).
|
if err := r.db.WithContext(ctx).Model(&entities.Ingredient{}).
|
||||||
Joins("JOIN ingredients ON inventory.product_id = ingredients.id").
|
Where("outlet_id = ? AND is_active = ? AND stock = 0", outletID, true).
|
||||||
Where("inventory.outlet_id = ? AND inventory.quantity = 0", outletID).
|
|
||||||
Count(&zeroStockIngredients).Error; err != nil {
|
Count(&zeroStockIngredients).Error; err != nil {
|
||||||
return nil, fmt.Errorf("failed to count zero stock ingredients: %w", err)
|
return nil, fmt.Errorf("failed to count zero stock ingredients: %w", err)
|
||||||
}
|
}
|
||||||
summary.ZeroStockIngredients = int(zeroStockIngredients)
|
summary.ZeroStockIngredients = int(zeroStockIngredients)
|
||||||
|
|
||||||
// Get total value
|
// Get total sold from inventory movements
|
||||||
|
totalSoldProducts, totalSoldIngredients, err := r.getTotalSoldFromMovements(ctx, outletID, dateFrom, dateTo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get total sold from movements: %w", err)
|
||||||
|
}
|
||||||
|
summary.TotalSoldProducts = totalSoldProducts
|
||||||
|
summary.TotalSoldIngredients = totalSoldIngredients
|
||||||
|
|
||||||
totalValue, err := r.GetTotalValueByOutlet(ctx, outletID)
|
totalValue, err := r.GetTotalValueByOutlet(ctx, outletID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get total value: %w", err)
|
return nil, fmt.Errorf("failed to get total value: %w", err)
|
||||||
@ -429,23 +428,20 @@ func (r *InventoryRepositoryImpl) GetInventoryReportSummary(ctx context.Context,
|
|||||||
func (r *InventoryRepositoryImpl) GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter) (*models.InventoryReportDetail, error) {
|
func (r *InventoryRepositoryImpl) GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter) (*models.InventoryReportDetail, error) {
|
||||||
report := &models.InventoryReportDetail{}
|
report := &models.InventoryReportDetail{}
|
||||||
|
|
||||||
// Get summary
|
|
||||||
if filter.OutletID != nil {
|
if filter.OutletID != nil {
|
||||||
summary, err := r.GetInventoryReportSummary(ctx, *filter.OutletID)
|
summary, err := r.GetInventoryReportSummary(ctx, *filter.OutletID, filter.DateFrom, filter.DateTo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get report summary: %w", err)
|
return nil, fmt.Errorf("failed to get report summary: %w", err)
|
||||||
}
|
}
|
||||||
report.Summary = summary
|
report.Summary = summary
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get products details
|
|
||||||
products, err := r.getInventoryProductsDetails(ctx, filter)
|
products, err := r.getInventoryProductsDetails(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get products details: %w", err)
|
return nil, fmt.Errorf("failed to get products details: %w", err)
|
||||||
}
|
}
|
||||||
report.Products = products
|
report.Products = products
|
||||||
|
|
||||||
// Get ingredients details
|
|
||||||
ingredients, err := r.getInventoryIngredientsDetails(ctx, filter)
|
ingredients, err := r.getInventoryIngredientsDetails(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get ingredients details: %w", err)
|
return nil, fmt.Errorf("failed to get ingredients details: %w", err)
|
||||||
@ -457,6 +453,7 @@ func (r *InventoryRepositoryImpl) GetInventoryReportDetails(ctx context.Context,
|
|||||||
|
|
||||||
// getInventoryProductsDetails retrieves detailed product inventory information
|
// getInventoryProductsDetails retrieves detailed product inventory information
|
||||||
func (r *InventoryRepositoryImpl) getInventoryProductsDetails(ctx context.Context, filter *models.InventoryReportFilter) ([]*models.InventoryProductDetail, error) {
|
func (r *InventoryRepositoryImpl) getInventoryProductsDetails(ctx context.Context, filter *models.InventoryReportFilter) ([]*models.InventoryProductDetail, error) {
|
||||||
|
// Build the base query
|
||||||
query := r.db.WithContext(ctx).Table("inventory").
|
query := r.db.WithContext(ctx).Table("inventory").
|
||||||
Select(`
|
Select(`
|
||||||
inventory.id,
|
inventory.id,
|
||||||
@ -472,7 +469,7 @@ func (r *InventoryRepositoryImpl) getInventoryProductsDetails(ctx context.Contex
|
|||||||
Joins("JOIN products ON inventory.product_id = products.id").
|
Joins("JOIN products ON inventory.product_id = products.id").
|
||||||
Joins("LEFT JOIN categories ON products.category_id = categories.id").
|
Joins("LEFT JOIN categories ON products.category_id = categories.id").
|
||||||
Joins("LEFT JOIN product_variants ON products.id = product_variants.product_id").
|
Joins("LEFT JOIN product_variants ON products.id = product_variants.product_id").
|
||||||
Where("inventory.outlet_id = ? AND products.has_ingredients = false", filter.OutletID)
|
Where("inventory.outlet_id = ?", filter.OutletID)
|
||||||
|
|
||||||
// Apply filters
|
// Apply filters
|
||||||
if filter.CategoryID != nil {
|
if filter.CategoryID != nil {
|
||||||
@ -499,7 +496,8 @@ func (r *InventoryRepositoryImpl) getInventoryProductsDetails(ctx context.Contex
|
|||||||
|
|
||||||
query = query.Order("products.name ASC")
|
query = query.Order("products.name ASC")
|
||||||
|
|
||||||
var results []struct {
|
// Execute the base query first
|
||||||
|
var baseResults []struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
ProductID uuid.UUID
|
ProductID uuid.UUID
|
||||||
ProductName string
|
ProductName string
|
||||||
@ -511,12 +509,33 @@ func (r *InventoryRepositoryImpl) getInventoryProductsDetails(ctx context.Contex
|
|||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := query.Find(&results).Error; err != nil {
|
if err := query.Find(&baseResults).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now get total sold for each product
|
||||||
var products []*models.InventoryProductDetail
|
var products []*models.InventoryProductDetail
|
||||||
for _, result := range results {
|
for _, result := range baseResults {
|
||||||
|
var totalSold float64
|
||||||
|
|
||||||
|
// Query total sold for this specific product
|
||||||
|
soldQuery := r.db.WithContext(ctx).Model(&entities.InventoryMovement{}).
|
||||||
|
Select("COALESCE(SUM(ABS(quantity)), 0)").
|
||||||
|
Where("item_id = ? AND outlet_id = ? AND item_type = ? AND movement_type = ? AND quantity < 0",
|
||||||
|
result.ProductID, filter.OutletID, "PRODUCT", entities.InventoryMovementTypeSale)
|
||||||
|
|
||||||
|
// Apply date filters if provided
|
||||||
|
if filter.DateFrom != nil {
|
||||||
|
soldQuery = soldQuery.Where("created_at >= ?", *filter.DateFrom)
|
||||||
|
}
|
||||||
|
if filter.DateTo != nil {
|
||||||
|
soldQuery = soldQuery.Where("created_at <= ?", *filter.DateTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := soldQuery.Scan(&totalSold).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get total sold for product %s: %w", result.ProductID, err)
|
||||||
|
}
|
||||||
|
|
||||||
categoryName := ""
|
categoryName := ""
|
||||||
if result.CategoryName != nil {
|
if result.CategoryName != nil {
|
||||||
categoryName = *result.CategoryName
|
categoryName = *result.CategoryName
|
||||||
@ -531,6 +550,7 @@ func (r *InventoryRepositoryImpl) getInventoryProductsDetails(ctx context.Contex
|
|||||||
ReorderLevel: result.ReorderLevel,
|
ReorderLevel: result.ReorderLevel,
|
||||||
UnitCost: result.UnitCost,
|
UnitCost: result.UnitCost,
|
||||||
TotalValue: result.TotalValue,
|
TotalValue: result.TotalValue,
|
||||||
|
TotalSold: totalSold,
|
||||||
IsLowStock: result.Quantity <= result.ReorderLevel && result.Quantity > 0,
|
IsLowStock: result.Quantity <= result.ReorderLevel && result.Quantity > 0,
|
||||||
IsZeroStock: result.Quantity == 0,
|
IsZeroStock: result.Quantity == 0,
|
||||||
UpdatedAt: result.UpdatedAt,
|
UpdatedAt: result.UpdatedAt,
|
||||||
@ -543,28 +563,26 @@ func (r *InventoryRepositoryImpl) getInventoryProductsDetails(ctx context.Contex
|
|||||||
|
|
||||||
// getInventoryIngredientsDetails retrieves detailed ingredient inventory information
|
// getInventoryIngredientsDetails retrieves detailed ingredient inventory information
|
||||||
func (r *InventoryRepositoryImpl) getInventoryIngredientsDetails(ctx context.Context, filter *models.InventoryReportFilter) ([]*models.InventoryIngredientDetail, error) {
|
func (r *InventoryRepositoryImpl) getInventoryIngredientsDetails(ctx context.Context, filter *models.InventoryReportFilter) ([]*models.InventoryIngredientDetail, error) {
|
||||||
query := r.db.WithContext(ctx).Table("inventory").
|
query := r.db.WithContext(ctx).Table("ingredients").
|
||||||
Select(`
|
Select(`
|
||||||
inventory.id,
|
ingredients.id,
|
||||||
inventory.product_id as ingredient_id,
|
ingredients.id as ingredient_id,
|
||||||
ingredients.name as ingredient_name,
|
ingredients.name as ingredient_name,
|
||||||
units.name as unit_name,
|
units.name as unit_name,
|
||||||
inventory.quantity,
|
ingredients.stock as quantity,
|
||||||
inventory.reorder_level,
|
0 as reorder_level,
|
||||||
ingredients.cost as unit_cost,
|
ingredients.cost as unit_cost,
|
||||||
(ingredients.cost * inventory.quantity) as total_value,
|
(ingredients.cost * ingredients.stock) as total_value,
|
||||||
inventory.updated_at
|
ingredients.updated_at
|
||||||
`).
|
`).
|
||||||
Joins("JOIN ingredients ON inventory.product_id = ingredients.id").
|
|
||||||
Joins("LEFT JOIN units ON ingredients.unit_id = units.id").
|
Joins("LEFT JOIN units ON ingredients.unit_id = units.id").
|
||||||
Where("inventory.outlet_id = ?", filter.OutletID)
|
Where("ingredients.outlet_id = ? AND ingredients.is_active = ?", filter.OutletID, true)
|
||||||
|
|
||||||
// Apply filters
|
|
||||||
if filter.ShowLowStock != nil && *filter.ShowLowStock {
|
if filter.ShowLowStock != nil && *filter.ShowLowStock {
|
||||||
query = query.Where("inventory.quantity <= inventory.reorder_level AND inventory.quantity > 0")
|
query = query.Where("ingredients.stock <= 0 AND ingredients.stock > 0")
|
||||||
}
|
}
|
||||||
if filter.ShowZeroStock != nil && *filter.ShowZeroStock {
|
if filter.ShowZeroStock != nil && *filter.ShowZeroStock {
|
||||||
query = query.Where("inventory.quantity = 0")
|
query = query.Where("ingredients.stock = 0")
|
||||||
}
|
}
|
||||||
if filter.Search != nil && *filter.Search != "" {
|
if filter.Search != nil && *filter.Search != "" {
|
||||||
searchTerm := "%" + *filter.Search + "%"
|
searchTerm := "%" + *filter.Search + "%"
|
||||||
@ -581,24 +599,42 @@ func (r *InventoryRepositoryImpl) getInventoryIngredientsDetails(ctx context.Con
|
|||||||
|
|
||||||
query = query.Order("ingredients.name ASC")
|
query = query.Order("ingredients.name ASC")
|
||||||
|
|
||||||
var results []struct {
|
var baseResults []struct {
|
||||||
ID uuid.UUID
|
ID uuid.UUID
|
||||||
IngredientID uuid.UUID
|
IngredientID uuid.UUID
|
||||||
IngredientName string
|
IngredientName string
|
||||||
UnitName *string
|
UnitName *string
|
||||||
Quantity int
|
Quantity float64
|
||||||
ReorderLevel int
|
ReorderLevel int
|
||||||
UnitCost float64
|
UnitCost float64
|
||||||
TotalValue float64
|
TotalValue float64
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := query.Find(&results).Error; err != nil {
|
if err := query.Find(&baseResults).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var ingredients []*models.InventoryIngredientDetail
|
var ingredients []*models.InventoryIngredientDetail
|
||||||
for _, result := range results {
|
for _, result := range baseResults {
|
||||||
|
var totalSold float64
|
||||||
|
|
||||||
|
soldQuery := r.db.WithContext(ctx).Model(&entities.InventoryMovement{}).
|
||||||
|
Select("COALESCE(SUM(ABS(quantity)), 0)").
|
||||||
|
Where("item_id = ? AND outlet_id = ? AND item_type = ? AND movement_type = ? AND quantity < 0",
|
||||||
|
result.IngredientID, filter.OutletID, "INGREDIENT", entities.InventoryMovementTypeSale)
|
||||||
|
|
||||||
|
if filter.DateFrom != nil {
|
||||||
|
soldQuery = soldQuery.Where("created_at >= ?", *filter.DateFrom)
|
||||||
|
}
|
||||||
|
if filter.DateTo != nil {
|
||||||
|
soldQuery = soldQuery.Where("created_at <= ?", *filter.DateTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := soldQuery.Scan(&totalSold).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get total sold for ingredient %s: %w", result.IngredientID, err)
|
||||||
|
}
|
||||||
|
|
||||||
unitName := ""
|
unitName := ""
|
||||||
if result.UnitName != nil {
|
if result.UnitName != nil {
|
||||||
unitName = *result.UnitName
|
unitName = *result.UnitName
|
||||||
@ -609,11 +645,12 @@ func (r *InventoryRepositoryImpl) getInventoryIngredientsDetails(ctx context.Con
|
|||||||
IngredientID: result.IngredientID,
|
IngredientID: result.IngredientID,
|
||||||
IngredientName: result.IngredientName,
|
IngredientName: result.IngredientName,
|
||||||
UnitName: unitName,
|
UnitName: unitName,
|
||||||
Quantity: result.Quantity,
|
Quantity: int(result.Quantity),
|
||||||
ReorderLevel: result.ReorderLevel,
|
ReorderLevel: result.ReorderLevel,
|
||||||
UnitCost: result.UnitCost,
|
UnitCost: result.UnitCost,
|
||||||
TotalValue: result.TotalValue,
|
TotalValue: result.TotalValue,
|
||||||
IsLowStock: result.Quantity <= result.ReorderLevel && result.Quantity > 0,
|
TotalSold: totalSold,
|
||||||
|
IsLowStock: result.Quantity <= 0 && result.Quantity > 0,
|
||||||
IsZeroStock: result.Quantity == 0,
|
IsZeroStock: result.Quantity == 0,
|
||||||
UpdatedAt: result.UpdatedAt,
|
UpdatedAt: result.UpdatedAt,
|
||||||
}
|
}
|
||||||
@ -622,3 +659,43 @@ func (r *InventoryRepositoryImpl) getInventoryIngredientsDetails(ctx context.Con
|
|||||||
|
|
||||||
return ingredients, nil
|
return ingredients, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getTotalSoldFromMovements calculates total sold quantities from inventory movements
|
||||||
|
func (r *InventoryRepositoryImpl) getTotalSoldFromMovements(ctx context.Context, outletID uuid.UUID, dateFrom, dateTo *time.Time) (float64, float64, error) {
|
||||||
|
var totalSoldProducts float64
|
||||||
|
var totalSoldIngredients float64
|
||||||
|
|
||||||
|
// Build base query for products
|
||||||
|
productQuery := r.db.WithContext(ctx).Model(&entities.InventoryMovement{}).
|
||||||
|
Select("COALESCE(SUM(ABS(quantity)), 0)").
|
||||||
|
Where("outlet_id = ? AND item_type = ? AND movement_type = ? AND quantity < 0",
|
||||||
|
outletID, "PRODUCT", entities.InventoryMovementTypeSale)
|
||||||
|
|
||||||
|
// Build base query for ingredients
|
||||||
|
ingredientQuery := r.db.WithContext(ctx).Model(&entities.InventoryMovement{}).
|
||||||
|
Select("COALESCE(SUM(ABS(quantity)), 0)").
|
||||||
|
Where("outlet_id = ? AND item_type = ? AND movement_type = ? AND quantity < 0",
|
||||||
|
outletID, "INGREDIENT", entities.InventoryMovementTypeSale)
|
||||||
|
|
||||||
|
// Apply date range filters if provided
|
||||||
|
if dateFrom != nil {
|
||||||
|
productQuery = productQuery.Where("created_at >= ?", *dateFrom)
|
||||||
|
ingredientQuery = ingredientQuery.Where("created_at >= ?", *dateFrom)
|
||||||
|
}
|
||||||
|
if dateTo != nil {
|
||||||
|
productQuery = productQuery.Where("created_at <= ?", *dateTo)
|
||||||
|
ingredientQuery = ingredientQuery.Where("created_at <= ?", *dateTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total sold products from inventory movements
|
||||||
|
if err := productQuery.Scan(&totalSoldProducts).Error; err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("failed to get total sold products: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total sold ingredients from inventory movements
|
||||||
|
if err := ingredientQuery.Scan(&totalSoldIngredients).Error; err != nil {
|
||||||
|
return 0, 0, fmt.Errorf("failed to get total sold ingredients: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalSoldProducts, totalSoldIngredients, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -206,7 +206,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
inventory.GET("/low-stock/:outlet_id", r.inventoryHandler.GetLowStockItems)
|
inventory.GET("/low-stock/:outlet_id", r.inventoryHandler.GetLowStockItems)
|
||||||
inventory.GET("/zero-stock/:outlet_id", r.inventoryHandler.GetZeroStockItems)
|
inventory.GET("/zero-stock/:outlet_id", r.inventoryHandler.GetZeroStockItems)
|
||||||
inventory.GET("/report/summary/:outlet_id", r.inventoryHandler.GetInventoryReportSummary)
|
inventory.GET("/report/summary/:outlet_id", r.inventoryHandler.GetInventoryReportSummary)
|
||||||
inventory.GET("/report/details", r.inventoryHandler.GetInventoryReportDetails)
|
inventory.GET("/report/details/:outlet_id", r.inventoryHandler.GetInventoryReportDetails)
|
||||||
}
|
}
|
||||||
|
|
||||||
orders := protected.Group("/orders")
|
orders := protected.Group("/orders")
|
||||||
|
|||||||
@ -23,7 +23,7 @@ type InventoryService interface {
|
|||||||
AdjustInventory(ctx context.Context, req *contract.AdjustInventoryRequest) *contract.Response
|
AdjustInventory(ctx context.Context, req *contract.AdjustInventoryRequest) *contract.Response
|
||||||
GetLowStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response
|
GetLowStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response
|
||||||
GetZeroStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response
|
GetZeroStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response
|
||||||
GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID) (*contract.InventoryReportSummaryResponse, error)
|
GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID, dateFrom, dateTo *time.Time) (*contract.InventoryReportSummaryResponse, error)
|
||||||
GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*contract.InventoryReportDetailResponse, error)
|
GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*contract.InventoryReportDetailResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,8 +188,8 @@ func (s *InventoryServiceImpl) GetZeroStockItems(ctx context.Context, outletID u
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetInventoryReportSummary returns summary statistics for inventory report
|
// GetInventoryReportSummary returns summary statistics for inventory report
|
||||||
func (s *InventoryServiceImpl) GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID) (*contract.InventoryReportSummaryResponse, error) {
|
func (s *InventoryServiceImpl) GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID, dateFrom, dateTo *time.Time) (*contract.InventoryReportSummaryResponse, error) {
|
||||||
summary, err := s.inventoryProcessor.GetInventoryReportSummary(ctx, outletID, organizationID)
|
summary, err := s.inventoryProcessor.GetInventoryReportSummary(ctx, outletID, organizationID, dateFrom, dateTo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -202,6 +202,8 @@ func (s *InventoryServiceImpl) GetInventoryReportSummary(ctx context.Context, ou
|
|||||||
LowStockIngredients: summary.LowStockIngredients,
|
LowStockIngredients: summary.LowStockIngredients,
|
||||||
ZeroStockProducts: summary.ZeroStockProducts,
|
ZeroStockProducts: summary.ZeroStockProducts,
|
||||||
ZeroStockIngredients: summary.ZeroStockIngredients,
|
ZeroStockIngredients: summary.ZeroStockIngredients,
|
||||||
|
TotalSoldProducts: summary.TotalSoldProducts,
|
||||||
|
TotalSoldIngredients: summary.TotalSoldIngredients,
|
||||||
OutletID: summary.OutletID.String(),
|
OutletID: summary.OutletID.String(),
|
||||||
OutletName: summary.OutletName,
|
OutletName: summary.OutletName,
|
||||||
GeneratedAt: summary.GeneratedAt.Format(time.RFC3339),
|
GeneratedAt: summary.GeneratedAt.Format(time.RFC3339),
|
||||||
@ -217,7 +219,6 @@ func (s *InventoryServiceImpl) GetInventoryReportDetails(ctx context.Context, fi
|
|||||||
|
|
||||||
response := &contract.InventoryReportDetailResponse{}
|
response := &contract.InventoryReportDetailResponse{}
|
||||||
|
|
||||||
// Transform summary
|
|
||||||
if report.Summary != nil {
|
if report.Summary != nil {
|
||||||
response.Summary = &contract.InventoryReportSummaryResponse{
|
response.Summary = &contract.InventoryReportSummaryResponse{
|
||||||
TotalProducts: report.Summary.TotalProducts,
|
TotalProducts: report.Summary.TotalProducts,
|
||||||
@ -227,13 +228,14 @@ func (s *InventoryServiceImpl) GetInventoryReportDetails(ctx context.Context, fi
|
|||||||
LowStockIngredients: report.Summary.LowStockIngredients,
|
LowStockIngredients: report.Summary.LowStockIngredients,
|
||||||
ZeroStockProducts: report.Summary.ZeroStockProducts,
|
ZeroStockProducts: report.Summary.ZeroStockProducts,
|
||||||
ZeroStockIngredients: report.Summary.ZeroStockIngredients,
|
ZeroStockIngredients: report.Summary.ZeroStockIngredients,
|
||||||
|
TotalSoldProducts: report.Summary.TotalSoldProducts,
|
||||||
|
TotalSoldIngredients: report.Summary.TotalSoldIngredients,
|
||||||
OutletID: report.Summary.OutletID.String(),
|
OutletID: report.Summary.OutletID.String(),
|
||||||
OutletName: report.Summary.OutletName,
|
OutletName: report.Summary.OutletName,
|
||||||
GeneratedAt: report.Summary.GeneratedAt.Format(time.RFC3339),
|
GeneratedAt: report.Summary.GeneratedAt.Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform products
|
|
||||||
response.Products = make([]*contract.InventoryProductDetailResponse, len(report.Products))
|
response.Products = make([]*contract.InventoryProductDetailResponse, len(report.Products))
|
||||||
for i, product := range report.Products {
|
for i, product := range report.Products {
|
||||||
response.Products[i] = &contract.InventoryProductDetailResponse{
|
response.Products[i] = &contract.InventoryProductDetailResponse{
|
||||||
@ -245,6 +247,7 @@ func (s *InventoryServiceImpl) GetInventoryReportDetails(ctx context.Context, fi
|
|||||||
ReorderLevel: product.ReorderLevel,
|
ReorderLevel: product.ReorderLevel,
|
||||||
UnitCost: product.UnitCost,
|
UnitCost: product.UnitCost,
|
||||||
TotalValue: product.TotalValue,
|
TotalValue: product.TotalValue,
|
||||||
|
TotalSold: product.TotalSold,
|
||||||
IsLowStock: product.IsLowStock,
|
IsLowStock: product.IsLowStock,
|
||||||
IsZeroStock: product.IsZeroStock,
|
IsZeroStock: product.IsZeroStock,
|
||||||
UpdatedAt: product.UpdatedAt.Format(time.RFC3339),
|
UpdatedAt: product.UpdatedAt.Format(time.RFC3339),
|
||||||
@ -263,6 +266,7 @@ func (s *InventoryServiceImpl) GetInventoryReportDetails(ctx context.Context, fi
|
|||||||
ReorderLevel: ingredient.ReorderLevel,
|
ReorderLevel: ingredient.ReorderLevel,
|
||||||
UnitCost: ingredient.UnitCost,
|
UnitCost: ingredient.UnitCost,
|
||||||
TotalValue: ingredient.TotalValue,
|
TotalValue: ingredient.TotalValue,
|
||||||
|
TotalSold: ingredient.TotalSold,
|
||||||
IsLowStock: ingredient.IsLowStock,
|
IsLowStock: ingredient.IsLowStock,
|
||||||
IsZeroStock: ingredient.IsZeroStock,
|
IsZeroStock: ingredient.IsZeroStock,
|
||||||
UpdatedAt: ingredient.UpdatedAt.Format(time.RFC3339),
|
UpdatedAt: ingredient.UpdatedAt.Format(time.RFC3339),
|
||||||
|
|||||||
@ -27,8 +27,6 @@ func ParseDateToJakartaTime(dateStr string) (*time.Time, error) {
|
|||||||
return &jakartaTime, nil
|
return &jakartaTime, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseDateToJakartaTimeEndOfDay parses a date string in DD-MM-YYYY format and converts it to Jakarta timezone
|
|
||||||
// Returns end of day (23:59:59.999999999) in Jakarta timezone
|
|
||||||
func ParseDateToJakartaTimeEndOfDay(dateStr string) (*time.Time, error) {
|
func ParseDateToJakartaTimeEndOfDay(dateStr string) (*time.Time, error) {
|
||||||
if dateStr == "" {
|
if dateStr == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user