Add Inventory Report

This commit is contained in:
Aditya Siregar
2025-08-14 00:38:26 +07:00
parent bb9a81c7c1
commit 7adba2c8f5
9 changed files with 177 additions and 85 deletions
+4 -6
View File
@@ -3,6 +3,7 @@ package processor
import (
"context"
"fmt"
"time"
"apskel-pos-be/internal/mappers"
"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)
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
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)
}
@@ -257,9 +258,7 @@ func (p *InventoryProcessorImpl) GetZeroStock(ctx context.Context, outletID, org
return responses, nil
}
// GetInventoryReportSummary returns summary statistics for inventory report
func (p *InventoryProcessorImpl) GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID) (*models.InventoryReportSummary, error) {
// Verify outlet belongs to organization
func (p *InventoryProcessorImpl) GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID, dateFrom, dateTo *time.Time) (*models.InventoryReportSummary, error) {
outlet, err := p.outletRepo.GetByID(ctx, outletID)
if err != nil {
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")
}
summary, err := p.inventoryRepo.GetInventoryReportSummary(ctx, outletID)
summary, err := p.inventoryRepo.GetInventoryReportSummary(ctx, outletID, dateFrom, dateTo)
if err != nil {
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")
}
// Verify outlet belongs to organization
outlet, err := p.outletRepo.GetByID(ctx, *filter.OutletID)
if err != nil {
return nil, fmt.Errorf("outlet not found: %w", err)
+4 -20
View File
@@ -1334,18 +1334,8 @@ type ingredientRecipeData struct {
// 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) {
// 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 err error
if item.ProductVariantID != nil {
recipes, err = p.productRecipeRepo.GetByProductAndVariantID(ctx, item.ProductID, item.ProductVariantID, order.OrganizationID)
} else {
@@ -1363,7 +1353,6 @@ func (p *OrderProcessorImpl) prepareIngredientRecipeData(ctx context.Context, it
var ingredientUpdates []*entities.Ingredient
var movements []*entities.InventoryMovement
// Process each ingredient in the recipe
for _, recipe := range recipes {
ingredientData, err := p.prepareIngredientRecipeItem(ctx, recipe, item, order, payment)
if err != nil {
@@ -1388,20 +1377,15 @@ type ingredientRecipeItem struct {
// 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) {
// Calculate total ingredient quantity needed
totalIngredientQuantity := recipe.Quantity * float64(item.Quantity)
// Get current ingredient details
currentIngredient, err := p.ingredientRepo.GetByID(ctx, recipe.IngredientID, order.OrganizationID)
if err != nil {
return nil, fmt.Errorf("failed to get ingredient: %w", err)
}
// For ingredients, we typically don't track quantity in the ingredient entity itself
// Instead, we create inventory movement records to track consumption
// The ingredient entity remains unchanged, but we track the movement
currentIngredient.Stock -= totalIngredientQuantity
// Prepare movement record
movement := &entities.InventoryMovement{
OrganizationID: order.OrganizationID,
OutletID: order.OutletID,
@@ -1409,8 +1393,8 @@ func (p *OrderProcessorImpl) prepareIngredientRecipeItem(ctx context.Context, re
ItemType: "INGREDIENT",
MovementType: entities.InventoryMovementTypeIngredient,
Quantity: -totalIngredientQuantity,
PreviousQuantity: 0, // We don't track current quantity in ingredient entity
NewQuantity: 0, // We don't track current quantity in ingredient entity
PreviousQuantity: 0,
NewQuantity: 0,
UnitCost: currentIngredient.Cost,
TotalCost: totalIngredientQuantity * currentIngredient.Cost,
ReferenceType: func() *entities.InventoryMovementReferenceType {