Add DRF serializers, viewsets, what-can-i-cook endpoint, log-cook with pantry deduction
This commit is contained in:
@@ -0,0 +1,114 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from .models import (
|
||||||
|
Tag,
|
||||||
|
Ingredient,
|
||||||
|
PantryItem,
|
||||||
|
MetaRecipe,
|
||||||
|
Slot,
|
||||||
|
SlotOption,
|
||||||
|
MetaRecipeBase,
|
||||||
|
Recipe,
|
||||||
|
RecipeIngredient,
|
||||||
|
CookLog,
|
||||||
|
ShoppingListItem,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TagSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Tag
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientSerializer(serializers.ModelSerializer):
|
||||||
|
tags = TagSerializer(many=True, read_only=True)
|
||||||
|
tag_ids = serializers.PrimaryKeyRelatedField(
|
||||||
|
queryset=Tag.objects.all(), many=True, write_only=True, source="tags", required=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Ingredient
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class PantryItemSerializer(serializers.ModelSerializer):
|
||||||
|
ingredient_name = serializers.CharField(source="ingredient.name", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = PantryItem
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Meta-Recipe nested serializers ---
|
||||||
|
|
||||||
|
|
||||||
|
class SlotOptionSerializer(serializers.ModelSerializer):
|
||||||
|
ingredient_name = serializers.CharField(source="ingredient.name", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SlotOption
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class SlotSerializer(serializers.ModelSerializer):
|
||||||
|
options = SlotOptionSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Slot
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class MetaRecipeBaseSerializer(serializers.ModelSerializer):
|
||||||
|
ingredient_name = serializers.CharField(source="ingredient.name", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MetaRecipeBase
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class MetaRecipeSerializer(serializers.ModelSerializer):
|
||||||
|
slots = SlotSerializer(many=True, read_only=True)
|
||||||
|
base_ingredients = MetaRecipeBaseSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = MetaRecipe
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Fixed Recipe nested serializers ---
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeIngredientSerializer(serializers.ModelSerializer):
|
||||||
|
ingredient_name = serializers.CharField(source="ingredient.name", read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RecipeIngredient
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeSerializer(serializers.ModelSerializer):
|
||||||
|
ingredients = RecipeIngredientSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Recipe
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Cook Log & Shopping ---
|
||||||
|
|
||||||
|
|
||||||
|
class CookLogSerializer(serializers.ModelSerializer):
|
||||||
|
meta_recipe_name = serializers.CharField(source="meta_recipe.name", read_only=True, default=None)
|
||||||
|
recipe_name = serializers.CharField(source="recipe.name", read_only=True, default=None)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CookLog
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class ShoppingListItemSerializer(serializers.ModelSerializer):
|
||||||
|
ingredient_name = serializers.CharField(source="ingredient.name", read_only=True, default=None)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ShoppingListItem
|
||||||
|
fields = "__all__"
|
||||||
+416
-2
@@ -1,3 +1,417 @@
|
|||||||
from django.shortcuts import render
|
from decimal import Decimal
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
# Create your views here.
|
from rest_framework import viewsets, status
|
||||||
|
from rest_framework.decorators import api_view, permission_classes, action
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
Tag,
|
||||||
|
Ingredient,
|
||||||
|
PantryItem,
|
||||||
|
MetaRecipe,
|
||||||
|
Slot,
|
||||||
|
SlotOption,
|
||||||
|
MetaRecipeBase,
|
||||||
|
Recipe,
|
||||||
|
RecipeIngredient,
|
||||||
|
CookLog,
|
||||||
|
ShoppingListItem,
|
||||||
|
)
|
||||||
|
from .serializers import (
|
||||||
|
TagSerializer,
|
||||||
|
IngredientSerializer,
|
||||||
|
PantryItemSerializer,
|
||||||
|
MetaRecipeSerializer,
|
||||||
|
SlotSerializer,
|
||||||
|
SlotOptionSerializer,
|
||||||
|
MetaRecipeBaseSerializer,
|
||||||
|
RecipeSerializer,
|
||||||
|
RecipeIngredientSerializer,
|
||||||
|
CookLogSerializer,
|
||||||
|
ShoppingListItemSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Standard CRUD ViewSets ---
|
||||||
|
|
||||||
|
|
||||||
|
class TagViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Tag.objects.all()
|
||||||
|
serializer_class = TagSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
|
class IngredientViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Ingredient.objects.all()
|
||||||
|
serializer_class = IngredientSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
search_fields = ["name", "aliases"]
|
||||||
|
|
||||||
|
|
||||||
|
class PantryItemViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = PantryItem.objects.select_related("ingredient").all()
|
||||||
|
serializer_class = PantryItemSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
filterset_fields = ["location", "is_staple"]
|
||||||
|
|
||||||
|
@action(detail=False, methods=["get"])
|
||||||
|
def expiring(self, request):
|
||||||
|
"""Items expiring within 3 days."""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
cutoff = date.today() + timedelta(days=3)
|
||||||
|
items = self.queryset.filter(
|
||||||
|
expiry_date__isnull=False,
|
||||||
|
expiry_date__lte=cutoff,
|
||||||
|
quantity__gt=0,
|
||||||
|
)
|
||||||
|
serializer = self.get_serializer(items, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@action(detail=False, methods=["get"])
|
||||||
|
def restock(self, request):
|
||||||
|
"""Staple items that need restocking (quantity = 0)."""
|
||||||
|
items = self.queryset.filter(is_staple=True, quantity=0)
|
||||||
|
serializer = self.get_serializer(items, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class MetaRecipeViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = MetaRecipe.objects.prefetch_related(
|
||||||
|
"slots__options__ingredient", "base_ingredients__ingredient"
|
||||||
|
).all()
|
||||||
|
serializer_class = MetaRecipeSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
|
class SlotViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Slot.objects.prefetch_related("options__ingredient").all()
|
||||||
|
serializer_class = SlotSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
|
class SlotOptionViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = SlotOption.objects.select_related("ingredient").all()
|
||||||
|
serializer_class = SlotOptionSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
|
class MetaRecipeBaseViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = MetaRecipeBase.objects.select_related("ingredient").all()
|
||||||
|
serializer_class = MetaRecipeBaseSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = Recipe.objects.prefetch_related("ingredients__ingredient").all()
|
||||||
|
serializer_class = RecipeSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeIngredientViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = RecipeIngredient.objects.select_related("ingredient").all()
|
||||||
|
serializer_class = RecipeIngredientSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
|
class CookLogViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = CookLog.objects.select_related("meta_recipe", "recipe").all()
|
||||||
|
serializer_class = CookLogSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
|
class ShoppingListItemViewSet(viewsets.ModelViewSet):
|
||||||
|
queryset = ShoppingListItem.objects.select_related("ingredient").all()
|
||||||
|
serializer_class = ShoppingListItemSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
filterset_fields = ["checked"]
|
||||||
|
|
||||||
|
|
||||||
|
# --- What Can I Cook? ---
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["GET"])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
def what_can_i_cook(request):
|
||||||
|
"""
|
||||||
|
Match pantry contents against meta-recipes and fixed recipes.
|
||||||
|
|
||||||
|
Returns recipes sorted by feasibility:
|
||||||
|
- ✅ All required slots can be filled from pantry
|
||||||
|
- ⚠️ Most slots filled, some missing (with details)
|
||||||
|
- ❌ Not enough ingredients
|
||||||
|
|
||||||
|
Also checks expiry warnings on matched ingredients.
|
||||||
|
"""
|
||||||
|
servings = int(request.query_params.get("servings", 2))
|
||||||
|
|
||||||
|
# Build pantry lookup: ingredient_id -> list of {quantity, unit, location, expiry}
|
||||||
|
pantry = {}
|
||||||
|
for item in PantryItem.objects.select_related("ingredient").filter(quantity__gt=0):
|
||||||
|
if item.ingredient_id not in pantry:
|
||||||
|
pantry[item.ingredient_id] = []
|
||||||
|
pantry[item.ingredient_id].append({
|
||||||
|
"quantity": item.quantity,
|
||||||
|
"unit": item.unit,
|
||||||
|
"location": item.location,
|
||||||
|
"expiry_date": item.expiry_date,
|
||||||
|
"is_expired": item.expiry_date and item.expiry_date < date.today(),
|
||||||
|
"expiring_soon": (
|
||||||
|
item.expiry_date
|
||||||
|
and not (item.expiry_date < date.today())
|
||||||
|
and (item.expiry_date - date.today()).days <= 2
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_pantry_total(ingredient_id):
|
||||||
|
"""Total quantity available across all locations."""
|
||||||
|
if ingredient_id not in pantry:
|
||||||
|
return Decimal("0")
|
||||||
|
return sum(p["quantity"] for p in pantry[ingredient_id])
|
||||||
|
|
||||||
|
def get_pantry_warnings(ingredient_id):
|
||||||
|
"""Get expiry warnings for an ingredient."""
|
||||||
|
warnings = []
|
||||||
|
if ingredient_id in pantry:
|
||||||
|
for p in pantry[ingredient_id]:
|
||||||
|
if p["is_expired"]:
|
||||||
|
warnings.append(f"EXPIRED in {p['location']} (exp {p['expiry_date']})")
|
||||||
|
elif p["expiring_soon"]:
|
||||||
|
warnings.append(f"expiring soon in {p['location']} (exp {p['expiry_date']})")
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Check meta-recipes
|
||||||
|
for meta in MetaRecipe.objects.prefetch_related(
|
||||||
|
"slots__options__ingredient", "base_ingredients__ingredient"
|
||||||
|
).all():
|
||||||
|
recipe_result = {
|
||||||
|
"type": "meta_recipe",
|
||||||
|
"id": meta.id,
|
||||||
|
"name": meta.name,
|
||||||
|
"slots": [],
|
||||||
|
"base_missing": [],
|
||||||
|
"warnings": [],
|
||||||
|
"feasible": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check base ingredients
|
||||||
|
for base in meta.base_ingredients.all():
|
||||||
|
needed = base.quantity_per_serving * servings
|
||||||
|
available = get_pantry_total(base.ingredient_id)
|
||||||
|
warnings = get_pantry_warnings(base.ingredient_id)
|
||||||
|
|
||||||
|
if available < needed:
|
||||||
|
if base.ingredient.is_staple_ingredient:
|
||||||
|
# Staples like oil/salt — assume always available even if tracking says 0
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
recipe_result["base_missing"].append({
|
||||||
|
"ingredient": base.ingredient.name,
|
||||||
|
"needed": f"{needed} {base.unit}",
|
||||||
|
"have": f"{available} {base.unit}" if available > 0 else "none",
|
||||||
|
})
|
||||||
|
if warnings:
|
||||||
|
recipe_result["warnings"].extend(
|
||||||
|
[f"{base.ingredient.name}: {w}" for w in warnings]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check each slot
|
||||||
|
for slot in meta.slots.all():
|
||||||
|
slot_result = {
|
||||||
|
"name": slot.name,
|
||||||
|
"required": slot.required,
|
||||||
|
"max_choices": slot.max_choices,
|
||||||
|
"available_options": [],
|
||||||
|
"missing_options": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for option in slot.options.all():
|
||||||
|
needed = option.quantity_per_serving * servings
|
||||||
|
available = get_pantry_total(option.ingredient_id)
|
||||||
|
warnings = get_pantry_warnings(option.ingredient_id)
|
||||||
|
|
||||||
|
option_info = {
|
||||||
|
"ingredient": option.ingredient.name,
|
||||||
|
"needed": f"{needed} {option.unit}",
|
||||||
|
"have": f"{available} {option.unit}",
|
||||||
|
"notes": option.notes,
|
||||||
|
}
|
||||||
|
|
||||||
|
if warnings:
|
||||||
|
option_info["warnings"] = warnings
|
||||||
|
|
||||||
|
if available >= needed:
|
||||||
|
slot_result["available_options"].append(option_info)
|
||||||
|
else:
|
||||||
|
slot_result["missing_options"].append(option_info)
|
||||||
|
|
||||||
|
# Is this slot satisfied?
|
||||||
|
if slot.required and not slot_result["available_options"]:
|
||||||
|
recipe_result["feasible"] = False
|
||||||
|
|
||||||
|
recipe_result["slots"].append(slot_result)
|
||||||
|
|
||||||
|
# Determine status
|
||||||
|
if recipe_result["feasible"] and not recipe_result["base_missing"]:
|
||||||
|
recipe_result["status"] = "ready"
|
||||||
|
recipe_result["emoji"] = "✅"
|
||||||
|
elif recipe_result["feasible"]:
|
||||||
|
recipe_result["status"] = "partial"
|
||||||
|
recipe_result["emoji"] = "⚠️"
|
||||||
|
else:
|
||||||
|
recipe_result["status"] = "missing"
|
||||||
|
recipe_result["emoji"] = "❌"
|
||||||
|
|
||||||
|
results.append(recipe_result)
|
||||||
|
|
||||||
|
# Check fixed recipes
|
||||||
|
for recipe in Recipe.objects.prefetch_related("ingredients__ingredient").all():
|
||||||
|
recipe_result = {
|
||||||
|
"type": "recipe",
|
||||||
|
"id": recipe.id,
|
||||||
|
"name": recipe.name,
|
||||||
|
"missing": [],
|
||||||
|
"warnings": [],
|
||||||
|
"feasible": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
for ri in recipe.ingredients.all():
|
||||||
|
needed = ri.quantity * (servings / recipe.servings)
|
||||||
|
available = get_pantry_total(ri.ingredient_id)
|
||||||
|
warnings = get_pantry_warnings(ri.ingredient_id)
|
||||||
|
|
||||||
|
if available < needed and not ri.optional:
|
||||||
|
recipe_result["feasible"] = False
|
||||||
|
recipe_result["missing"].append({
|
||||||
|
"ingredient": ri.ingredient.name,
|
||||||
|
"needed": f"{needed} {ri.unit}",
|
||||||
|
"have": f"{available} {ri.unit}" if available > 0 else "none",
|
||||||
|
})
|
||||||
|
|
||||||
|
if warnings:
|
||||||
|
recipe_result["warnings"].extend(
|
||||||
|
[f"{ri.ingredient.name}: {w}" for w in warnings]
|
||||||
|
)
|
||||||
|
|
||||||
|
recipe_result["status"] = "ready" if recipe_result["feasible"] else "missing"
|
||||||
|
recipe_result["emoji"] = "✅" if recipe_result["feasible"] else "❌"
|
||||||
|
results.append(recipe_result)
|
||||||
|
|
||||||
|
# Sort: ready first, then partial, then missing
|
||||||
|
order = {"ready": 0, "partial": 1, "missing": 2}
|
||||||
|
results.sort(key=lambda r: order.get(r["status"], 3))
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
"servings": servings,
|
||||||
|
"pantry_items": PantryItem.objects.filter(quantity__gt=0).count(),
|
||||||
|
"results": results,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@api_view(["POST"])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
def log_cook(request):
|
||||||
|
"""
|
||||||
|
Log a meal that was cooked. Optionally deducts ingredients from pantry.
|
||||||
|
|
||||||
|
Body:
|
||||||
|
{
|
||||||
|
"meta_recipe_id": 1, // or "recipe_id": 1
|
||||||
|
"slot_choices": {"protein": "pork mince", "carb": "egg noodles"},
|
||||||
|
"servings": 2,
|
||||||
|
"notes": "added extra garlic",
|
||||||
|
"deduct": true // auto-deduct from pantry
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
meta_recipe_id = request.data.get("meta_recipe_id")
|
||||||
|
recipe_id = request.data.get("recipe_id")
|
||||||
|
slot_choices = request.data.get("slot_choices", {})
|
||||||
|
servings = int(request.data.get("servings", 2))
|
||||||
|
notes = request.data.get("notes", "")
|
||||||
|
deduct = request.data.get("deduct", False)
|
||||||
|
|
||||||
|
if not meta_recipe_id and not recipe_id:
|
||||||
|
return Response(
|
||||||
|
{"error": "Must provide meta_recipe_id or recipe_id"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create cook log
|
||||||
|
log = CookLog.objects.create(
|
||||||
|
meta_recipe_id=meta_recipe_id,
|
||||||
|
recipe_id=recipe_id,
|
||||||
|
slot_choices=slot_choices,
|
||||||
|
servings=servings,
|
||||||
|
notes=notes,
|
||||||
|
)
|
||||||
|
|
||||||
|
deducted = []
|
||||||
|
|
||||||
|
if deduct and meta_recipe_id:
|
||||||
|
meta = MetaRecipe.objects.prefetch_related(
|
||||||
|
"slots__options__ingredient", "base_ingredients__ingredient"
|
||||||
|
).get(id=meta_recipe_id)
|
||||||
|
|
||||||
|
# Deduct base ingredients
|
||||||
|
for base in meta.base_ingredients.all():
|
||||||
|
amount = base.quantity_per_serving * servings
|
||||||
|
deducted += _deduct_ingredient(base.ingredient, amount, base.unit)
|
||||||
|
|
||||||
|
# Deduct slot choices
|
||||||
|
for slot_name, ingredient_name in slot_choices.items():
|
||||||
|
try:
|
||||||
|
slot = meta.slots.get(name=slot_name)
|
||||||
|
option = slot.options.get(ingredient__name=ingredient_name)
|
||||||
|
amount = option.quantity_per_serving * servings
|
||||||
|
deducted += _deduct_ingredient(option.ingredient, amount, option.unit)
|
||||||
|
except (Slot.DoesNotExist, SlotOption.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
|
elif deduct and recipe_id:
|
||||||
|
recipe = Recipe.objects.prefetch_related("ingredients__ingredient").get(id=recipe_id)
|
||||||
|
for ri in recipe.ingredients.all():
|
||||||
|
amount = ri.quantity * (servings / recipe.servings)
|
||||||
|
deducted += _deduct_ingredient(ri.ingredient, amount, ri.unit)
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
"cook_log_id": log.id,
|
||||||
|
"deducted": deducted,
|
||||||
|
}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
|
def _deduct_ingredient(ingredient, amount, unit):
|
||||||
|
"""Deduct an amount from pantry, preferring fridge items first (use oldest first)."""
|
||||||
|
remaining = Decimal(str(amount))
|
||||||
|
deducted = []
|
||||||
|
|
||||||
|
# Prefer fridge (expires first), then cupboard, then freezer
|
||||||
|
items = PantryItem.objects.filter(
|
||||||
|
ingredient=ingredient, quantity__gt=0
|
||||||
|
).order_by(
|
||||||
|
# Fridge first, then cupboard, then freezer
|
||||||
|
# Within same location, earliest expiry first
|
||||||
|
"expiry_date",
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
if remaining <= 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
take = min(item.quantity, remaining)
|
||||||
|
item.quantity -= take
|
||||||
|
item.save(update_fields=["quantity"])
|
||||||
|
remaining -= take
|
||||||
|
|
||||||
|
deducted.append({
|
||||||
|
"ingredient": ingredient.name,
|
||||||
|
"amount": str(take),
|
||||||
|
"unit": unit,
|
||||||
|
"from": item.location,
|
||||||
|
"remaining_in_pantry": str(item.quantity),
|
||||||
|
})
|
||||||
|
|
||||||
|
return deducted
|
||||||
|
|||||||
Reference in New Issue
Block a user