add food substitutions
This commit is contained in:
parent
5e3f94fcf7
commit
6ef25b604b
@ -419,12 +419,47 @@ class RecipeSearch():
|
||||
if not self._makenow:
|
||||
return
|
||||
shopping_users = [*self._request.user.get_shopping_share(), self._request.user]
|
||||
|
||||
onhand_filter = (
|
||||
Q(steps__ingredients__food__onhand_users__in=shopping_users, steps__ingredients__food__ignore_shopping=False) # food onhand
|
||||
| Q(steps__ingredients__food__substitute__onhand_users__in=shopping_users, steps__ingredients__food__substitute__ignore_shopping=False) # or substitute food onhand
|
||||
| Q(steps__ingredients__food__in=self.__children_substitute_filter(shopping_users))
|
||||
| Q(steps__ingredients__food__in=self.__sibling_substitute_filter(shopping_users))
|
||||
)
|
||||
self._queryset = self._queryset.annotate(
|
||||
count_food=Count('steps__ingredients__food'),
|
||||
count_onhand=Count('pk', filter=Q(steps__ingredients__food__onhand_users__in=shopping_users, steps__ingredients__food__ignore_shopping=False)),
|
||||
count_onhand=Count('pk', filter=Q(onhand_filter)),
|
||||
count_ignore=Count('pk', filter=Q(steps__ingredients__food__ignore_shopping=True))
|
||||
).annotate(missingfood=F('count_food')-F('count_onhand')-F('count_ignore')).filter(missingfood=0)
|
||||
|
||||
@staticmethod
|
||||
def __children_substitute_filter(shopping_users=None):
|
||||
children_onhand_subquery = Food.objects.filter(
|
||||
path__startswith=Substr(OuterRef('path'), 1, Food.steplen*OuterRef('depth')),
|
||||
depth__gt=OuterRef('depth'),
|
||||
onhand_users__in=shopping_users
|
||||
).annotate(child_onhand=Count((Substr(OuterRef('path'), 1, Food.steplen*OuterRef('depth'))), distinct=True)).values('child_onhand')
|
||||
return Food.objects.exclude( # list of foods that are onhand and children of: foods that are not onhand and are set to use children as substitutes
|
||||
Q(onhand_users__in=shopping_users)
|
||||
| Q(ignore_shopping=True)
|
||||
| Q(substitute__onhand_users__in=shopping_users)
|
||||
).exclude(depth=1, numchild=0).filter(substitute_children=True
|
||||
).annotate(child_onhand=Coalesce(Subquery(children_onhand_subquery), 0))
|
||||
|
||||
@staticmethod
|
||||
def __sibling_substitute_filter(shopping_users=None):
|
||||
sibling_onhand_subquery = Food.objects.filter(
|
||||
path__startswith=Substr(OuterRef('path'), 1, Food.steplen*(OuterRef('depth')-1)),
|
||||
depth=OuterRef('depth'),
|
||||
onhand_users__in=shopping_users
|
||||
).annotate(sibling_onhand=Count(Substr(OuterRef('path'), 1, Food.steplen*(OuterRef('depth')-1)), distinct=True)).values('sibling_onhand')
|
||||
return Food.objects.exclude( # list of foods that are onhand and siblings of: foods that are not onhand and are set to use siblings as substitutes
|
||||
Q(onhand_users__in=shopping_users)
|
||||
| Q(ignore_shopping=True)
|
||||
| Q(substitute__onhand_users__in=shopping_users)
|
||||
).exclude(depth=1, numchild=0).filter(substitute_siblings=True
|
||||
).annotate(sibling_onhand=Coalesce(Subquery(sibling_onhand_subquery), 0))
|
||||
|
||||
|
||||
class RecipeFacet():
|
||||
class CacheEmpty(Exception):
|
||||
@ -605,7 +640,7 @@ class RecipeFacet():
|
||||
|
||||
def _recipe_count_queryset(self, field, depth=1, steplen=4):
|
||||
return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path')}, id__in=self._recipe_list, space=self._request.space
|
||||
).values(child=Substr(f'{field}__path', 1, steplen)
|
||||
).values(child=Substr(f'{field}__path', 1, steplen*depth)
|
||||
).annotate(count=Count('pk', distinct=True)).values('count')
|
||||
|
||||
def _keyword_queryset(self, queryset, keyword=None):
|
||||
|
19
cookbook/migrations/0170_alter_ingredient_unit.py
Normal file
19
cookbook/migrations/0170_alter_ingredient_unit.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.2.11 on 2022-02-02 19:36
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0169_auto_20220121_1427'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ingredient',
|
||||
name='unit',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='cookbook.unit'),
|
||||
),
|
||||
]
|
34
cookbook/migrations/0171_auto_20220202_1340.py
Normal file
34
cookbook/migrations/0171_auto_20220202_1340.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Generated by Django 3.2.11 on 2022-02-02 19:40
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cookbook', '0170_alter_ingredient_unit'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='substitute',
|
||||
field=models.ManyToManyField(blank=True, related_name='_cookbook_food_substitute_+', to='cookbook.Food'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='substitute_children',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='food',
|
||||
name='substitute_siblings',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ingredient',
|
||||
name='unit',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='cookbook.unit'),
|
||||
),
|
||||
]
|
@ -488,6 +488,7 @@ class Unit(ExportModelOperationsMixin('unit'), models.Model, PermissionModelMixi
|
||||
|
||||
|
||||
class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
||||
# TODO when savings a food as substitute children - assume children and descednats are also substitutes for siblings
|
||||
# exclude fields not implemented yet
|
||||
inheritable_fields = FoodInheritField.objects.exclude(field__in=['diet', 'substitute', 'substitute_children', 'substitute_siblings'])
|
||||
|
||||
@ -501,6 +502,9 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
||||
onhand_users = models.ManyToManyField(User, blank=True)
|
||||
description = models.TextField(default='', blank=True)
|
||||
inherit_fields = models.ManyToManyField(FoodInheritField, blank=True)
|
||||
substitute = models.ManyToManyField("self", blank=True)
|
||||
substitute_siblings = models.BooleanField(default=False)
|
||||
substitute_children = models.BooleanField(default=False)
|
||||
|
||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||
objects = ScopedManager(space='space', _manager_class=TreeManager)
|
||||
@ -560,9 +564,9 @@ class Food(ExportModelOperationsMixin('food'), TreeModel, PermissionModelMixin):
|
||||
|
||||
|
||||
class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, PermissionModelMixin):
|
||||
# a pre-delete signal on Food checks if the Ingredient is part of a Step, if it is raises a ProtectedError instead of cascading the delete
|
||||
# delete method on Food and Unit checks if they are part of a Recipe, if it is raises a ProtectedError instead of cascading the delete
|
||||
food = models.ForeignKey(Food, on_delete=models.CASCADE, null=True, blank=True)
|
||||
unit = models.ForeignKey(Unit, on_delete=models.PROTECT, null=True, blank=True)
|
||||
unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
amount = models.DecimalField(default=0, decimal_places=16, max_digits=32)
|
||||
note = models.CharField(max_length=256, null=True, blank=True)
|
||||
is_header = models.BooleanField(default=False)
|
||||
|
@ -4,7 +4,8 @@ from decimal import Decimal
|
||||
from gettext import gettext as _
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Avg, QuerySet, Sum
|
||||
from django.db.models import Avg, Q, QuerySet, Sum, Value
|
||||
from django.db.models.functions import Substr
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from drf_writable_nested import UniqueFieldsMixin, WritableNestedModelSerializer
|
||||
@ -375,6 +376,13 @@ class RecipeSimpleSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = ['id', 'name', 'url']
|
||||
|
||||
|
||||
class FoodSimpleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Food
|
||||
fields = ('id', 'name')
|
||||
read_only_fields = ['id', 'name']
|
||||
|
||||
|
||||
class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedRecipeMixin):
|
||||
supermarket_category = SupermarketCategorySerializer(allow_null=True, required=False)
|
||||
recipe = RecipeSimpleSerializer(allow_null=True, required=False)
|
||||
@ -382,10 +390,25 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
shopping = serializers.ReadOnlyField(source='shopping_status')
|
||||
inherit_fields = FoodInheritFieldSerializer(many=True, allow_null=True, required=False)
|
||||
food_onhand = CustomOnHandField(required=False, allow_null=True)
|
||||
substitute_onhand = serializers.SerializerMethodField('get_substitute_onhand')
|
||||
substitute = FoodSimpleSerializer(many=True, allow_null=True, required=False)
|
||||
|
||||
recipe_filter = 'steps__ingredients__food'
|
||||
images = ['recipe__image']
|
||||
|
||||
def get_substitute_onhand(self, obj):
|
||||
shared_users = None
|
||||
if request := self.context.get('request', None):
|
||||
shared_users = getattr(request, '_shared_users', None)
|
||||
if shared_users is None:
|
||||
shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id]
|
||||
filter = Q(id__in=obj.substitute.all())
|
||||
if obj.substitute_siblings:
|
||||
filter |= Q(path__startswith=obj.path[:Food.steplen*(obj.depth-1)], depth=obj.depth)
|
||||
if obj.substitute_children:
|
||||
filter |= Q(path__startswith=obj.path, depth__gt=obj.depth)
|
||||
return Food.objects.filter(filter).filter(onhand_users__id__in=shared_users).exists()
|
||||
|
||||
# def get_shopping_status(self, obj):
|
||||
# return ShoppingListEntry.objects.filter(space=obj.space, food=obj, checked=False).count() > 0
|
||||
|
||||
@ -437,7 +460,8 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
model = Food
|
||||
fields = (
|
||||
'id', 'name', 'description', 'shopping', 'recipe', 'food_onhand', 'supermarket_category',
|
||||
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping'
|
||||
'image', 'parent', 'numchild', 'numrecipe', 'inherit_fields', 'full_name', 'ignore_shopping',
|
||||
'substitute', 'substitute_siblings', 'substitute_children', 'substitute_onhand'
|
||||
)
|
||||
read_only_fields = ('id', 'numchild', 'parent', 'image', 'numrecipe')
|
||||
|
||||
|
@ -90,7 +90,10 @@ def test_add(arg, request, obj_1):
|
||||
c = request.getfixturevalue(arg[0])
|
||||
r = c.post(
|
||||
reverse(LIST_URL),
|
||||
{'food': model_to_dict(obj_1.food), 'amount': 1},
|
||||
{'food': {
|
||||
'id': obj_1.food.__dict__['id'],
|
||||
'name': obj_1.food.__dict__['name'],
|
||||
}, 'amount': 1},
|
||||
content_type='application/json'
|
||||
)
|
||||
response = json.loads(r.content)
|
||||
|
@ -103,7 +103,10 @@ def test_add(arg, request, sle):
|
||||
c = request.getfixturevalue(arg[0])
|
||||
r = c.post(
|
||||
reverse(LIST_URL),
|
||||
{'food': model_to_dict(sle[0].food), 'amount': 1},
|
||||
{'food': {
|
||||
'id': sle[0].food.__dict__['id'],
|
||||
'name': sle[0].food.__dict__['name'],
|
||||
}, 'amount': 1},
|
||||
content_type='application/json'
|
||||
)
|
||||
response = json.loads(r.content)
|
||||
|
@ -655,7 +655,7 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
QueryParam(name='new', description=_('Returns new results first in search results. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='timescooked', description=_('Filter recipes cooked X times or more. Negative values returns cooked less than X times'), qtype='int'),
|
||||
QueryParam(name='lastcooked', description=_('Filter recipes last cooked on or after YYYY-MM-DD. Prepending ''-'' filters on or before date.')),
|
||||
QueryParam(name='makenow', description=_('Filter recipes that can be made with OnHand food. [''true''/''<b>false</b>'']'), qtype='int'),
|
||||
QueryParam(name='makenow', description=_('Filter recipes that can be made with OnHand food. [''true''/''<b>false</b>'']')),
|
||||
]
|
||||
schema = QueryParamAutoSchema()
|
||||
|
||||
|
@ -417,7 +417,6 @@ export default {
|
||||
// TODO: make this generic
|
||||
let params = { pageSize: 50, random: true }
|
||||
params[this.this_recipe_param] = item.id
|
||||
console.log("RECIPE PARAM", this.this_recipe_param, params, item.id)
|
||||
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params)
|
||||
.then((result) => {
|
||||
parent = this.findCard(item.id, this["items_" + col])
|
||||
|
@ -915,7 +915,6 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
console.log(screen.height)
|
||||
this.getShoppingList()
|
||||
this.getSupermarkets()
|
||||
this.getShoppingCategories()
|
||||
@ -1406,7 +1405,6 @@ export default {
|
||||
window.removeEventListener("offline", this.updateOnlineStatus)
|
||||
},
|
||||
addRecipeToShopping() {
|
||||
console.log(this.new_recipe)
|
||||
this.$bvModal.show(`shopping_${this.new_recipe.id}`)
|
||||
},
|
||||
finishShopping() {
|
||||
|
@ -100,7 +100,7 @@ export default {
|
||||
this.loadInitial()
|
||||
},
|
||||
methods: {
|
||||
loadInitial: function() {
|
||||
loadInitial: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
apiClient.listSupermarkets().then((results) => {
|
||||
this.supermarkets = results.data
|
||||
@ -110,7 +110,7 @@ export default {
|
||||
this.selectable_categories = this.categories
|
||||
})
|
||||
},
|
||||
selectedCategoriesChanged: function(data) {
|
||||
selectedCategoriesChanged: function (data) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
if ("removed" in data) {
|
||||
@ -133,23 +133,22 @@ export default {
|
||||
if ("moved" in data || "added" in data) {
|
||||
this.supermarket_categories.forEach((element, index) => {
|
||||
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === element.id)[0]
|
||||
console.log(relation)
|
||||
apiClient.partialUpdateSupermarketCategoryRelation(relation.id, { order: index })
|
||||
})
|
||||
}
|
||||
},
|
||||
selectedSupermarketChanged: function(supermarket, id) {
|
||||
selectedSupermarketChanged: function (supermarket, id) {
|
||||
this.supermarket_categories = []
|
||||
this.selectable_categories = this.categories
|
||||
|
||||
for (let i of supermarket.category_to_supermarket) {
|
||||
this.supermarket_categories.push(i.category)
|
||||
this.selectable_categories = this.selectable_categories.filter(function(el) {
|
||||
this.selectable_categories = this.selectable_categories.filter(function (el) {
|
||||
return el.id !== i.category.id
|
||||
})
|
||||
}
|
||||
},
|
||||
supermarketModalOk: function() {
|
||||
supermarketModalOk: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
if (this.selected_supermarket.new) {
|
||||
apiClient.createSupermarket({ name: this.selected_supermarket.name }).then((results) => {
|
||||
@ -160,7 +159,7 @@ export default {
|
||||
apiClient.partialUpdateSupermarket(this.selected_supermarket.id, { name: this.selected_supermarket.name })
|
||||
}
|
||||
},
|
||||
categoryModalOk: function() {
|
||||
categoryModalOk: function () {
|
||||
let apiClient = new ApiApiFactory()
|
||||
if (this.selected_category.new) {
|
||||
apiClient.createSupermarketCategory({ name: this.selected_category.name }).then((results) => {
|
||||
|
@ -1,13 +1,6 @@
|
||||
<template>
|
||||
<span>
|
||||
<b-button
|
||||
class="btn text-decoration-none fas px-1 py-0 border-0"
|
||||
variant="link"
|
||||
v-b-popover.hover.html
|
||||
:title="[onhand ? $t('FoodOnHand', { food: item.name }) : $t('FoodNotOnHand', { food: item.name })]"
|
||||
:class="[onhand ? 'text-success fa-clipboard-check' : 'text-muted fa-clipboard']"
|
||||
@click="toggleOnHand"
|
||||
/>
|
||||
<b-button v-if="!item.ignore_shopping" class="btn text-decoration-none fas px-1 py-0 border-0" variant="link" v-b-popover.hover.html :title="Title" :class="IconClass" @click="toggleOnHand" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@ -25,6 +18,26 @@ export default {
|
||||
onhand: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
Title: function () {
|
||||
if (this.onhand) {
|
||||
return this.$t("FoodOnHand", { food: this.item.name })
|
||||
} else if (this.item.substitute_onhand) {
|
||||
return this.$t("SubstituteOnHand")
|
||||
} else {
|
||||
return this.$t("FoodNotOnHand", { food: this.item.name })
|
||||
}
|
||||
},
|
||||
IconClass: function () {
|
||||
if (this.onhand) {
|
||||
return "text-success fa-clipboard-check"
|
||||
} else if (this.item.substitute_onhand) {
|
||||
return "text-warning fa-clipboard-check"
|
||||
} else {
|
||||
return "text-muted fa-clipboard"
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.onhand = this.item.food_onhand
|
||||
},
|
||||
|
@ -33,6 +33,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td v-else-if="show_shopping" class="text-right text-nowrap">
|
||||
<shopping-badge v-if="ingredient.food.ignore_shopping" :item="shoppingBadgeFood" />
|
||||
<b-button
|
||||
v-if="!ingredient.food.ignore_shopping"
|
||||
class="btn text-decoration-none fas fa-shopping-cart px-2 user-select-none"
|
||||
@ -56,10 +57,11 @@
|
||||
<script>
|
||||
import { calculateAmount, ResolveUrlMixin, ApiMixin } from "@/utils/utils"
|
||||
import OnHandBadge from "@/components/Badges/OnHand"
|
||||
import ShoppingBadge from "@/components/Badges/Shopping"
|
||||
|
||||
export default {
|
||||
name: "IngredientComponent",
|
||||
components: { OnHandBadge },
|
||||
components: { OnHandBadge, ShoppingBadge },
|
||||
props: {
|
||||
ingredient: Object,
|
||||
ingredient_factor: { type: Number, default: 1 },
|
||||
@ -87,6 +89,11 @@ export default {
|
||||
this.shop = this.ingredient?.shop
|
||||
},
|
||||
computed: {
|
||||
shoppingBadgeFood() {
|
||||
// shopping badge is hidden when ignore_shopping=true.
|
||||
// force true in this context to allow adding to shopping list from recipe view
|
||||
return { ...this.ingredient.food, ignore_shopping: false }
|
||||
},
|
||||
ShoppingPopover() {
|
||||
if (this.ingredient?.shopping_status == false) {
|
||||
return this.$t("NotInShopping", { food: this.ingredient.food.name })
|
||||
|
@ -56,7 +56,6 @@ import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
|
||||
import IngredientComponent from "@/components/IngredientComponent"
|
||||
import { ApiMixin, StandardToasts } from "@/utils/utils"
|
||||
import ShoppingListViewVue from "../apps/ShoppingListView/ShoppingListView.vue"
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
|
@ -16,8 +16,11 @@
|
||||
<small-text v-if="visibleCondition(f, 'smalltext')" :value="f.value" />
|
||||
</div>
|
||||
<template v-slot:modal-footer>
|
||||
<div class="row w-100 justify-content-end">
|
||||
<div class="col-auto">
|
||||
<div class="row w-100">
|
||||
<div class="col-6 align-self-end">
|
||||
<b-form-checkbox v-if="advancedForm" sm switch v-model="show_advanced">{{ $t("Advanced") }}</b-form-checkbox>
|
||||
</div>
|
||||
<div class="col-auto justify-content-end">
|
||||
<b-button class="mx-1" variant="secondary" v-on:click="cancelAction">{{ $t("Cancel") }}</b-button>
|
||||
<b-button class="mx-1" variant="primary" v-on:click="doAction">{{ form.ok_label }}</b-button>
|
||||
</div>
|
||||
@ -78,7 +81,8 @@ export default {
|
||||
form: {},
|
||||
dirty: false,
|
||||
special_handling: false,
|
||||
show_help: true,
|
||||
show_help: false,
|
||||
show_advanced: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -86,6 +90,13 @@ export default {
|
||||
this.$root.$on("change", this.storeValue) // bootstrap modal placed at document so have to listen at root of component
|
||||
},
|
||||
computed: {
|
||||
advancedForm() {
|
||||
return this.form.fields
|
||||
.map((x) => {
|
||||
return x?.advanced ?? false
|
||||
})
|
||||
.includes(true)
|
||||
},
|
||||
buttonLabel() {
|
||||
return this.buttons[this.action].label
|
||||
},
|
||||
@ -268,6 +279,11 @@ export default {
|
||||
visibleCondition(field, field_type) {
|
||||
let type_match = field?.type == field_type
|
||||
let checks = true
|
||||
let show_advanced = true
|
||||
if (field?.advanced) {
|
||||
show_advanced = this.show_advanced
|
||||
}
|
||||
|
||||
if (type_match && field?.condition) {
|
||||
const value = this.item1[field?.condition?.field]
|
||||
const preference = getUserPreference(field?.condition?.field)
|
||||
@ -294,7 +310,7 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
return type_match && checks
|
||||
return type_match && checks && show_advanced
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -55,7 +55,7 @@
|
||||
</b-input-group-prepend>
|
||||
|
||||
<b-form-spinbutton min="1" v-model="recipe_servings" inline style="height: 3em"></b-form-spinbutton>
|
||||
<CustomInputSpinButton v-model.number="recipe_servings" style="height: 3em" />
|
||||
<!-- <CustomInputSpinButton v-model.number="recipe_servings" style="height: 3em" /> -->
|
||||
|
||||
<b-input-group-append>
|
||||
<b-button variant="secondary" @click="$bvModal.hide(`shopping_${modal_id}`)">{{ $t("Cancel") }} </b-button>
|
||||
@ -76,11 +76,11 @@ const { ApiApiFactory } = require("@/utils/openapi/api")
|
||||
import { StandardToasts } from "@/utils/utils"
|
||||
import IngredientsCard from "@/components/IngredientsCard"
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import CustomInputSpinButton from "@/components/CustomInputSpinButton"
|
||||
// import CustomInputSpinButton from "@/components/CustomInputSpinButton"
|
||||
|
||||
export default {
|
||||
name: "ShoppingModal",
|
||||
components: { IngredientsCard, LoadingSpinner, CustomInputSpinButton },
|
||||
components: { IngredientsCard, LoadingSpinner },
|
||||
mixins: [],
|
||||
props: {
|
||||
recipe: { required: true, type: Object },
|
||||
|
@ -289,13 +289,11 @@
|
||||
"remember_search": "Remember Search",
|
||||
"remember_hours": "Hours to Remember",
|
||||
"tree_select": "Use Tree Selection",
|
||||
"OnHand_help": "Food is in inventory and will not be automatically added to a shopping list.",
|
||||
"OnHand_help": "Food is in inventory and will not be automatically added to a shopping list. Onhand status is shared with shopping users.",
|
||||
"ignore_shopping_help": "Never add food to the shopping list (e.g. water)",
|
||||
"shopping_category_help": "Supermarkets can be ordered and filtered by Shopping Category according to the layout of the aisles.",
|
||||
"food_recipe_help": "Linking a recipe here will include the linked recipe in any other recipe that use this food",
|
||||
"Foods": "Foods",
|
||||
"review_shopping": "Review shopping entries before saving",
|
||||
"view_recipe": "View Recipe",
|
||||
"enable_expert": "Enable Expert Mode",
|
||||
"expert_mode": "Expert Mode",
|
||||
"simple_mode": "Simple Mode",
|
||||
@ -326,6 +324,15 @@
|
||||
"make_now": "Make Now",
|
||||
"recipe_filter": "Recipe Filter",
|
||||
"book_filter_help": "Include recipes from recipe filter instead of assigning each recipe",
|
||||
"review_shopping": "Review shopping entries before saving",
|
||||
"view_recipe": "View Recipe",
|
||||
"filter": "Filter",
|
||||
"reset_children": "Reset Child Inheritance",
|
||||
"reset_children_help": "Overwrite all children with values from inherited fields."
|
||||
"reset_children_help": "Overwrite all children with values from inherited fields.",
|
||||
"substitute_help": "Substitutes are considered when searching for recipes that can be made with onhand ingredients.",
|
||||
"substitute_siblings_help": "All food that share a parent of this food are considered substitutes.",
|
||||
"substitute_children_help": "All food that are children of this food are considered substitutes.",
|
||||
"substitute_siblings": "Substitute Siblings",
|
||||
"substitute_children": "Substitute Children",
|
||||
"SubstituteOnHand": "You have a substitute on hand."
|
||||
}
|
||||
|
@ -76,7 +76,22 @@ export class Models {
|
||||
// REQUIRED: unordered array of fields that can be set during create
|
||||
create: {
|
||||
// if not defined partialUpdate will use the same parameters, prepending 'id'
|
||||
params: [["name", "description", "recipe", "food_onhand", "supermarket_category", "inherit", "inherit_fields", "ignore_shopping", "reset_inherit"]],
|
||||
params: [
|
||||
[
|
||||
"name",
|
||||
"description",
|
||||
"recipe",
|
||||
"food_onhand",
|
||||
"supermarket_category",
|
||||
"inherit",
|
||||
"inherit_fields",
|
||||
"ignore_shopping",
|
||||
"substitute",
|
||||
"substitute_siblings",
|
||||
"substitute_children",
|
||||
"reset_inherit",
|
||||
],
|
||||
],
|
||||
|
||||
form: {
|
||||
show_help: true,
|
||||
@ -126,8 +141,38 @@ export class Models {
|
||||
allow_create: true,
|
||||
help_text: i18n.t("shopping_category_help"),
|
||||
},
|
||||
substitute: {
|
||||
form_field: true,
|
||||
advanced: true,
|
||||
type: "lookup",
|
||||
multiple: true,
|
||||
field: "substitute",
|
||||
list: "FOOD",
|
||||
label: i18n.t("Substitutes"),
|
||||
allow_create: false,
|
||||
help_text: i18n.t("substitute_help"),
|
||||
},
|
||||
substitute_siblings: {
|
||||
form_field: true,
|
||||
advanced: true,
|
||||
type: "checkbox",
|
||||
field: "substitute_siblings",
|
||||
label: i18n.t("substitute_siblings"),
|
||||
help_text: i18n.t("substitute_siblings_help"),
|
||||
condition: { field: "parent", value: true, condition: "field_exists" },
|
||||
},
|
||||
substitute_children: {
|
||||
form_field: true,
|
||||
advanced: true,
|
||||
type: "checkbox",
|
||||
field: "substitute_children",
|
||||
label: i18n.t("substitute_children"),
|
||||
help_text: i18n.t("substitute_children_help"),
|
||||
condition: { field: "numchild", value: 0, condition: "gt" },
|
||||
},
|
||||
inherit_fields: {
|
||||
form_field: true,
|
||||
advanced: true,
|
||||
type: "lookup",
|
||||
multiple: true,
|
||||
field: "inherit_fields",
|
||||
@ -137,6 +182,7 @@ export class Models {
|
||||
},
|
||||
reset_inherit: {
|
||||
form_field: true,
|
||||
advanced: true,
|
||||
type: "checkbox",
|
||||
field: "reset_inherit",
|
||||
label: i18n.t("reset_children"),
|
||||
|
@ -307,6 +307,30 @@ export interface Food {
|
||||
* @memberof Food
|
||||
*/
|
||||
ignore_shopping?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {Array<FoodSubstitute>}
|
||||
* @memberof Food
|
||||
*/
|
||||
substitute?: Array<FoodSubstitute> | null;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof Food
|
||||
*/
|
||||
substitute_siblings?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof Food
|
||||
*/
|
||||
substitute_children?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Food
|
||||
*/
|
||||
substitute_onhand?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@ -423,6 +447,25 @@ export enum FoodShoppingUpdateDeleteEnum {
|
||||
True = 'true'
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface FoodSubstitute
|
||||
*/
|
||||
export interface FoodSubstitute {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof FoodSubstitute
|
||||
*/
|
||||
id?: number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof FoodSubstitute
|
||||
*/
|
||||
name?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -709,6 +752,30 @@ export interface IngredientFood {
|
||||
* @memberof IngredientFood
|
||||
*/
|
||||
ignore_shopping?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {Array<FoodSubstitute>}
|
||||
* @memberof IngredientFood
|
||||
*/
|
||||
substitute?: Array<FoodSubstitute> | null;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof IngredientFood
|
||||
*/
|
||||
substitute_siblings?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof IngredientFood
|
||||
*/
|
||||
substitute_children?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof IngredientFood
|
||||
*/
|
||||
substitute_onhand?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@ -5482,13 +5549,13 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
* @param {string} [_new] Returns new results first in search results. [true/<b>false</b>]
|
||||
* @param {number} [timescooked] Filter recipes cooked X times or more. Negative values returns cooked less than X times
|
||||
* @param {string} [lastcooked] Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on or before date.
|
||||
* @param {number} [makenow] Filter recipes that can be made with OnHand food. [true/<b>false</b>]
|
||||
* @param {string} [makenow] Filter recipes that can be made with OnHand food. [true/<b>false</b>]
|
||||
* @param {number} [page] A page number within the paginated result set.
|
||||
* @param {number} [pageSize] Number of results to return per page.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listRecipes: async (query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: number, page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
|
||||
listRecipes: async (query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: string, page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/recipe/`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
@ -10068,13 +10135,13 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
* @param {string} [_new] Returns new results first in search results. [true/<b>false</b>]
|
||||
* @param {number} [timescooked] Filter recipes cooked X times or more. Negative values returns cooked less than X times
|
||||
* @param {string} [lastcooked] Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on or before date.
|
||||
* @param {number} [makenow] Filter recipes that can be made with OnHand food. [true/<b>false</b>]
|
||||
* @param {string} [makenow] Filter recipes that can be made with OnHand food. [true/<b>false</b>]
|
||||
* @param {number} [page] A page number within the paginated result set.
|
||||
* @param {number} [pageSize] Number of results to return per page.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: number, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2004>> {
|
||||
async listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2004>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, foodsOr, foodsAnd, foodsOrNot, foodsAndNot, units, rating, books, booksOr, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, lastcooked, makenow, page, pageSize, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
@ -11823,13 +11890,13 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
* @param {string} [_new] Returns new results first in search results. [true/<b>false</b>]
|
||||
* @param {number} [timescooked] Filter recipes cooked X times or more. Negative values returns cooked less than X times
|
||||
* @param {string} [lastcooked] Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on or before date.
|
||||
* @param {number} [makenow] Filter recipes that can be made with OnHand food. [true/<b>false</b>]
|
||||
* @param {string} [makenow] Filter recipes that can be made with OnHand food. [true/<b>false</b>]
|
||||
* @param {number} [page] A page number within the paginated result set.
|
||||
* @param {number} [pageSize] Number of results to return per page.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: number, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2004> {
|
||||
listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2004> {
|
||||
return localVarFp.listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, foodsOr, foodsAnd, foodsOrNot, foodsAndNot, units, rating, books, booksOr, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, lastcooked, makenow, page, pageSize, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
@ -13605,14 +13672,14 @@ export class ApiApi extends BaseAPI {
|
||||
* @param {string} [_new] Returns new results first in search results. [true/<b>false</b>]
|
||||
* @param {number} [timescooked] Filter recipes cooked X times or more. Negative values returns cooked less than X times
|
||||
* @param {string} [lastcooked] Filter recipes last cooked on or after YYYY-MM-DD. Prepending - filters on or before date.
|
||||
* @param {number} [makenow] Filter recipes that can be made with OnHand food. [true/<b>false</b>]
|
||||
* @param {string} [makenow] Filter recipes that can be made with OnHand food. [true/<b>false</b>]
|
||||
* @param {number} [page] A page number within the paginated result set.
|
||||
* @param {number} [pageSize] Number of results to return per page.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiApi
|
||||
*/
|
||||
public listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: number, page?: number, pageSize?: number, options?: any) {
|
||||
public listRecipes(query?: string, keywords?: number, keywordsOr?: number, keywordsAnd?: number, keywordsOrNot?: number, keywordsAndNot?: number, foods?: number, foodsOr?: number, foodsAnd?: number, foodsOrNot?: number, foodsAndNot?: number, units?: number, rating?: number, books?: string, booksOr?: number, booksAnd?: number, booksOrNot?: number, booksAndNot?: number, internal?: string, random?: string, _new?: string, timescooked?: number, lastcooked?: string, makenow?: string, page?: number, pageSize?: number, options?: any) {
|
||||
return ApiApiFp(this.configuration).listRecipes(query, keywords, keywordsOr, keywordsAnd, keywordsOrNot, keywordsAndNot, foods, foodsOr, foodsAnd, foodsOrNot, foodsAndNot, units, rating, books, booksOr, booksAnd, booksOrNot, booksAndNot, internal, random, _new, timescooked, lastcooked, makenow, page, pageSize, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user