add new unit/food from shopping list
This commit is contained in:
parent
6e9d609fe0
commit
a217db5822
@ -228,6 +228,7 @@ class StorageForm(forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Deprecate
|
||||||
class RecipeBookEntryForm(forms.ModelForm):
|
class RecipeBookEntryForm(forms.ModelForm):
|
||||||
prefix = 'bookmark'
|
prefix = 'bookmark'
|
||||||
|
|
||||||
@ -480,7 +481,7 @@ class ShoppingPreferenceForm(forms.ModelForm):
|
|||||||
|
|
||||||
fields = (
|
fields = (
|
||||||
'shopping_share', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'mealplan_autoexclude_onhand',
|
'shopping_share', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'mealplan_autoexclude_onhand',
|
||||||
'mealplan_autoinclude_related', 'default_delay', 'filter_to_supermarket'
|
'mealplan_autoinclude_related', 'default_delay', 'filter_to_supermarket', 'shopping_recent_days'
|
||||||
)
|
)
|
||||||
|
|
||||||
help_texts = {
|
help_texts = {
|
||||||
@ -494,6 +495,7 @@ class ShoppingPreferenceForm(forms.ModelForm):
|
|||||||
'mealplan_autoexclude_onhand': _('When adding a meal plan to the shopping list (manually or automatically), exclude ingredients that are on hand.'),
|
'mealplan_autoexclude_onhand': _('When adding a meal plan to the shopping list (manually or automatically), exclude ingredients that are on hand.'),
|
||||||
'default_delay': _('Default number of hours to delay a shopping list entry.'),
|
'default_delay': _('Default number of hours to delay a shopping list entry.'),
|
||||||
'filter_to_supermarket': _('Filter shopping list to only include supermarket categories.'),
|
'filter_to_supermarket': _('Filter shopping list to only include supermarket categories.'),
|
||||||
|
'shopping_recent_days': _('Days of recent shopping list entries to display.'),
|
||||||
}
|
}
|
||||||
labels = {
|
labels = {
|
||||||
'shopping_share': _('Share Shopping List'),
|
'shopping_share': _('Share Shopping List'),
|
||||||
@ -503,6 +505,7 @@ class ShoppingPreferenceForm(forms.ModelForm):
|
|||||||
'mealplan_autoinclude_related': _('Include Related'),
|
'mealplan_autoinclude_related': _('Include Related'),
|
||||||
'default_delay': _('Default Delay Hours'),
|
'default_delay': _('Default Delay Hours'),
|
||||||
'filter_to_supermarket': _('Filter to Supermarket'),
|
'filter_to_supermarket': _('Filter to Supermarket'),
|
||||||
|
'shopping_recent_days': _('Recent Days')
|
||||||
}
|
}
|
||||||
|
|
||||||
widgets = {
|
widgets = {
|
||||||
|
@ -79,9 +79,6 @@ def is_object_shared(user, obj):
|
|||||||
# share checks for relevant objects
|
# share checks for relevant objects
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
if obj.__class__.__name__ == 'ShoppingListEntry':
|
|
||||||
# shopping lists are shared all or none and stored in user preferences
|
|
||||||
return obj.created_by in user.get_shopping_share()
|
|
||||||
else:
|
else:
|
||||||
return user in obj.get_shared()
|
return user in obj.get_shared()
|
||||||
|
|
||||||
|
@ -30,7 +30,6 @@ def search_recipes(request, queryset, params):
|
|||||||
search_steps = params.getlist('steps', [])
|
search_steps = params.getlist('steps', [])
|
||||||
search_units = params.get('units', None)
|
search_units = params.get('units', None)
|
||||||
|
|
||||||
# TODO I think default behavior should be 'AND' which is how most sites operate with facet/filters based on results
|
|
||||||
search_keywords_or = str2bool(params.get('keywords_or', True))
|
search_keywords_or = str2bool(params.get('keywords_or', True))
|
||||||
search_foods_or = str2bool(params.get('foods_or', True))
|
search_foods_or = str2bool(params.get('foods_or', True))
|
||||||
search_books_or = str2bool(params.get('books_or', True))
|
search_books_or = str2bool(params.get('books_or', True))
|
||||||
@ -202,20 +201,13 @@ def get_facet(qs=None, request=None, use_cache=True, hash_key=None):
|
|||||||
"""
|
"""
|
||||||
Gets an annotated list from a queryset.
|
Gets an annotated list from a queryset.
|
||||||
:param qs:
|
:param qs:
|
||||||
|
|
||||||
recipe queryset to build facets from
|
recipe queryset to build facets from
|
||||||
|
|
||||||
:param request:
|
:param request:
|
||||||
|
|
||||||
the web request that contains the necessary query parameters
|
the web request that contains the necessary query parameters
|
||||||
|
|
||||||
:param use_cache:
|
:param use_cache:
|
||||||
|
|
||||||
will find results in cache, if any, and return them or empty list.
|
will find results in cache, if any, and return them or empty list.
|
||||||
will save the list of recipes IDs in the cache for future processing
|
will save the list of recipes IDs in the cache for future processing
|
||||||
|
|
||||||
:param hash_key:
|
:param hash_key:
|
||||||
|
|
||||||
the cache key of the recipe list to process
|
the cache key of the recipe list to process
|
||||||
only evaluated if the use_cache parameter is false
|
only evaluated if the use_cache parameter is false
|
||||||
"""
|
"""
|
||||||
@ -290,7 +282,6 @@ def get_facet(qs=None, request=None, use_cache=True, hash_key=None):
|
|||||||
foods = Food.objects.filter(ingredient__step__recipe__in=recipe_list, space=request.space).annotate(recipe_count=Count('ingredient'))
|
foods = Food.objects.filter(ingredient__step__recipe__in=recipe_list, space=request.space).annotate(recipe_count=Count('ingredient'))
|
||||||
food_a = annotated_qs(foods, root=True, fill=True)
|
food_a = annotated_qs(foods, root=True, fill=True)
|
||||||
|
|
||||||
# TODO add rating facet
|
|
||||||
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list)
|
facets['Keywords'] = fill_annotated_parents(kw_a, keyword_list)
|
||||||
facets['Foods'] = fill_annotated_parents(food_a, food_list)
|
facets['Foods'] = fill_annotated_parents(food_a, food_list)
|
||||||
# TODO add book facet
|
# TODO add book facet
|
||||||
@ -363,8 +354,6 @@ def annotated_qs(qs, root=False, fill=False):
|
|||||||
dirty = False
|
dirty = False
|
||||||
current_node = node_queue[-1]
|
current_node = node_queue[-1]
|
||||||
depth = current_node.get_depth()
|
depth = current_node.get_depth()
|
||||||
# TODO if node is at the wrong depth for some reason this fails
|
|
||||||
# either create a 'fix node' page, or automatically move the node to the root
|
|
||||||
parent_id = current_node.parent
|
parent_id = current_node.parent
|
||||||
if root and depth > 1 and parent_id not in nodes_list:
|
if root and depth > 1 and parent_id not in nodes_list:
|
||||||
parent_id = current_node.parent
|
parent_id = current_node.parent
|
||||||
|
@ -15,14 +15,12 @@ from recipes import settings
|
|||||||
def shopping_helper(qs, request):
|
def shopping_helper(qs, request):
|
||||||
supermarket = request.query_params.get('supermarket', None)
|
supermarket = request.query_params.get('supermarket', None)
|
||||||
checked = request.query_params.get('checked', 'recent')
|
checked = request.query_params.get('checked', 'recent')
|
||||||
|
user = request.user
|
||||||
|
|
||||||
supermarket_order = ['food__supermarket_category__name', 'food__name']
|
supermarket_order = ['food__supermarket_category__name', 'food__name']
|
||||||
|
|
||||||
# TODO created either scheduled task or startup task to delete very old shopping list entries
|
# TODO created either scheduled task or startup task to delete very old shopping list entries
|
||||||
# TODO create user preference to define 'very old'
|
# TODO create user preference to define 'very old'
|
||||||
|
|
||||||
# qs = qs.annotate(supermarket_category=Coalesce(F('food__supermarket_category__name'), Value(_('Undefined'))))
|
|
||||||
# TODO add supermarket to API - order by category order
|
|
||||||
if supermarket:
|
if supermarket:
|
||||||
supermarket_categories = SupermarketCategoryRelation.objects.filter(supermarket=supermarket, category=OuterRef('food__supermarket_category'))
|
supermarket_categories = SupermarketCategoryRelation.objects.filter(supermarket=supermarket, category=OuterRef('food__supermarket_category'))
|
||||||
qs = qs.annotate(supermarket_order=Coalesce(Subquery(supermarket_categories.values('order')), Value(9999)))
|
qs = qs.annotate(supermarket_order=Coalesce(Subquery(supermarket_categories.values('order')), Value(9999)))
|
||||||
@ -33,8 +31,7 @@ def shopping_helper(qs, request):
|
|||||||
qs = qs.filter(checked=True)
|
qs = qs.filter(checked=True)
|
||||||
elif checked in ['recent']:
|
elif checked in ['recent']:
|
||||||
today_start = timezone.now().replace(hour=0, minute=0, second=0)
|
today_start = timezone.now().replace(hour=0, minute=0, second=0)
|
||||||
# TODO make recent a user setting
|
week_ago = today_start - timedelta(days=user.userpreference.shopping_recent_days)
|
||||||
week_ago = today_start - timedelta(days=7)
|
|
||||||
qs = qs.filter(Q(checked=False) | Q(completed_at__gte=week_ago))
|
qs = qs.filter(Q(checked=False) | Q(completed_at__gte=week_ago))
|
||||||
supermarket_order = ['checked'] + supermarket_order
|
supermarket_order = ['checked'] + supermarket_order
|
||||||
|
|
||||||
@ -51,7 +48,6 @@ def list_from_recipe(list_recipe=None, recipe=None, mealplan=None, servings=None
|
|||||||
:param ingredients: Ingredients, list of ingredient IDs to include on the shopping list. When not provided all ingredients will be used
|
:param ingredients: Ingredients, list of ingredient IDs to include on the shopping list. When not provided all ingredients will be used
|
||||||
:param append: If False will remove any entries not included with ingredients, when True will append ingredients to the shopping list
|
:param append: If False will remove any entries not included with ingredients, when True will append ingredients to the shopping list
|
||||||
"""
|
"""
|
||||||
# TODO cascade to related recipes
|
|
||||||
r = recipe or getattr(mealplan, 'recipe', None) or getattr(list_recipe, 'recipe', None)
|
r = recipe or getattr(mealplan, 'recipe', None) or getattr(list_recipe, 'recipe', None)
|
||||||
if not r:
|
if not r:
|
||||||
raise ValueError(_("You must supply a recipe or mealplan"))
|
raise ValueError(_("You must supply a recipe or mealplan"))
|
||||||
|
@ -2,13 +2,15 @@
|
|||||||
import annoying.fields
|
import annoying.fields
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.postgres.indexes import GinIndex
|
from django.contrib.postgres.indexes import GinIndex
|
||||||
from django.contrib.postgres.search import SearchVectorField, SearchVector
|
from django.contrib.postgres.search import SearchVector, SearchVectorField
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.db.models import deletion
|
from django.db.models import deletion
|
||||||
from django_scopes import scopes_disabled
|
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from cookbook.managers import DICTIONARY
|
from cookbook.managers import DICTIONARY
|
||||||
from cookbook.models import Recipe, Step, Index, PermissionModelMixin, nameSearchField, allSearchFields
|
from cookbook.models import (Index, PermissionModelMixin, Recipe, Step, allSearchFields,
|
||||||
|
nameSearchField)
|
||||||
|
|
||||||
|
|
||||||
def set_default_search_vector(apps, schema_editor):
|
def set_default_search_vector(apps, schema_editor):
|
||||||
@ -16,8 +18,6 @@ def set_default_search_vector(apps, schema_editor):
|
|||||||
return
|
return
|
||||||
language = DICTIONARY.get(translation.get_language(), 'simple')
|
language = DICTIONARY.get(translation.get_language(), 'simple')
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
# TODO this approach doesn't work terribly well if multiple languages are in use
|
|
||||||
# I'm also uncertain about forcing unaccent here
|
|
||||||
Recipe.objects.all().update(
|
Recipe.objects.all().update(
|
||||||
name_search_vector=SearchVector('name__unaccent', weight='A', config=language),
|
name_search_vector=SearchVector('name__unaccent', weight='A', config=language),
|
||||||
desc_search_vector=SearchVector('description__unaccent', weight='B', config=language)
|
desc_search_vector=SearchVector('description__unaccent', weight='B', config=language)
|
||||||
|
@ -157,5 +157,10 @@ class Migration(migrations.Migration):
|
|||||||
name='filter_to_supermarket',
|
name='filter_to_supermarket',
|
||||||
field=models.BooleanField(default=False),
|
field=models.BooleanField(default=False),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userpreference',
|
||||||
|
name='shopping_recent_days',
|
||||||
|
field=models.PositiveIntegerField(default=7),
|
||||||
|
),
|
||||||
migrations.RunPython(copy_values_to_sle),
|
migrations.RunPython(copy_values_to_sle),
|
||||||
]
|
]
|
||||||
|
@ -330,7 +330,8 @@ class UserPreference(models.Model, PermissionModelMixin):
|
|||||||
mealplan_autoexclude_onhand = models.BooleanField(default=True)
|
mealplan_autoexclude_onhand = models.BooleanField(default=True)
|
||||||
mealplan_autoinclude_related = models.BooleanField(default=True)
|
mealplan_autoinclude_related = models.BooleanField(default=True)
|
||||||
filter_to_supermarket = models.BooleanField(default=False)
|
filter_to_supermarket = models.BooleanField(default=False)
|
||||||
default_delay = models.IntegerField(default=4)
|
default_delay = models.DecimalField(default=4, max_digits=8, decimal_places=4)
|
||||||
|
shopping_recent_days = models.PositiveIntegerField(default=7)
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
|
space = models.ForeignKey(Space, on_delete=models.CASCADE, null=True)
|
||||||
|
@ -164,7 +164,7 @@ class UserPreferenceSerializer(serializers.ModelSerializer):
|
|||||||
'user', 'theme', 'nav_color', 'default_unit', 'default_page',
|
'user', 'theme', 'nav_color', 'default_unit', 'default_page',
|
||||||
'search_style', 'show_recent', 'plan_share', 'ingredient_decimals',
|
'search_style', 'show_recent', 'plan_share', 'ingredient_decimals',
|
||||||
'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_ignore_default', 'default_delay',
|
'comments', 'shopping_auto_sync', 'mealplan_autoadd_shopping', 'food_ignore_default', 'default_delay',
|
||||||
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share'
|
'mealplan_autoinclude_related', 'mealplan_autoexclude_onhand', 'shopping_share', 'shopping_recent_days'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ def skip_signal(signal_func):
|
|||||||
return _decorator
|
return _decorator
|
||||||
|
|
||||||
|
|
||||||
# TODO there is probably a way to generalize this
|
|
||||||
@receiver(post_save, sender=Recipe)
|
@receiver(post_save, sender=Recipe)
|
||||||
@skip_signal
|
@skip_signal
|
||||||
def update_recipe_search_vector(sender, instance=None, created=False, **kwargs):
|
def update_recipe_search_vector(sender, instance=None, created=False, **kwargs):
|
||||||
|
@ -1,15 +1,7 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %} {% comment %} TODO: Deprecate {% endcomment %} {% load django_tables2 %} {% load crispy_forms_tags %} {% load static %} {% load i18n %} {% block title
|
||||||
{% load django_tables2 %}
|
%}{% trans "Shopping List" %}{% endblock %} {% block extra_head %} {% include 'include/vue_base.html' %}
|
||||||
{% load crispy_forms_tags %}
|
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block title %}{% trans "Shopping List" %}{% endblock %}
|
<link rel="stylesheet" href="{% static 'css/vue-multiselect-bs4.min.css' %}" />
|
||||||
|
|
||||||
{% block extra_head %}
|
|
||||||
{% include 'include/vue_base.html' %}
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="{% static 'css/vue-multiselect-bs4.min.css' %}">
|
|
||||||
<script src="{% static 'js/vue-multiselect.min.js' %}"></script>
|
<script src="{% static 'js/vue-multiselect.min.js' %}"></script>
|
||||||
|
|
||||||
<script src="{% static 'js/Sortable.min.js' %}"></script>
|
<script src="{% static 'js/Sortable.min.js' %}"></script>
|
||||||
@ -19,6 +11,11 @@
|
|||||||
|
|
||||||
<script src="{% static 'js/js.cookie.min.js' %}"></script>
|
<script src="{% static 'js/js.cookie.min.js' %}"></script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{% static 'css/pretty-checkbox.min.css' %}" />
|
||||||
|
{% endblock %} {% block content %}
|
||||||
|
|
||||||
|
<script src="{% static 'js/js.cookie.min.js' %}"></script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="{% static 'css/pretty-checkbox.min.css' %}">
|
<link rel="stylesheet" href="{% static 'css/pretty-checkbox.min.css' %}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -32,10 +29,21 @@
|
|||||||
<b-form-checkbox switch size="lg" v-model="edit_mode"
|
<b-form-checkbox switch size="lg" v-model="edit_mode"
|
||||||
@change="$forceUpdate()">{% trans 'Edit' %}</b-form-checkbox>
|
@change="$forceUpdate()">{% trans 'Edit' %}</b-form-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="col col-md-9">
|
||||||
|
<h2>{% trans 'Shopping List' %}</h2>
|
||||||
|
</span>
|
||||||
|
<span class="col-md-3">
|
||||||
|
<a href="{% url 'view_shopping_new' %}" class="float-right">
|
||||||
|
<button class="btn btn-outline-secondary shadow-none"><i class="fas fa-star"></i> {% trans 'Try the new shopping list' %}</button>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<div class="col col-mdd-3 text-right">
|
||||||
|
<b-form-checkbox switch size="lg" v-model="edit_mode" @change="$forceUpdate()">{% trans 'Edit' %}</b-form-checkbox>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="shopping_list !== undefined">
|
<template v-if="shopping_list !== undefined">
|
||||||
|
|
||||||
<div class="text-center" v-if="loading">
|
<div class="text-center" v-if="loading">
|
||||||
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
|
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
|
||||||
<img class="spinner-tandoor" />
|
<img class="spinner-tandoor" />
|
||||||
@ -46,25 +54,16 @@
|
|||||||
<div v-else-if="edit_mode">
|
<div v-else-if="edit_mode">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header"><i class="fa fa-search"></i> {% trans 'Search' %}</div>
|
||||||
<i class="fa fa-search"></i> {% trans 'Search' %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<input type="text" class="form-control" v-model="recipe_query" @keyup="getRecipes"
|
<input type="text" class="form-control" v-model="recipe_query" @keyup="getRecipes" placeholder="{% trans 'Search Recipe' %}" />
|
||||||
placeholder="{% trans 'Search Recipe' %}">
|
|
||||||
<ul class="list-group" style="margin-top: 8px">
|
<ul class="list-group" style="margin-top: 8px">
|
||||||
<li class="list-group-item" v-for="x in recipes">
|
<li class="list-group-item" v-for="x in recipes">
|
||||||
<div class="row flex-row" style="padding-left: 0.5vw; padding-right: 0.5vw">
|
<div class="row flex-row" style="padding-left: 0.5vw; padding-right: 0.5vw">
|
||||||
<div class="flex-column flex-fill my-auto"><a v-bind:href="getRecipeUrl(x.id)"
|
<div class="flex-column flex-fill my-auto"><a v-bind:href="getRecipeUrl(x.id)" target="_blank" rel="nofollow norefferer">[[x.name]]</a></div>
|
||||||
target="_blank"
|
|
||||||
rel="nofollow norefferer">[[x.name]]</a>
|
|
||||||
</div>
|
|
||||||
<div class="flex-column align-self-end">
|
<div class="flex-column align-self-end">
|
||||||
<button class="btn btn-outline-primary shadow-none"
|
<button class="btn btn-outline-primary shadow-none" @click="addRecipeToList(x)"><i class="fa fa-plus"></i></button>
|
||||||
@click="addRecipeToList(x)"><i
|
|
||||||
class="fa fa-plus"></i></button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@ -75,41 +74,28 @@
|
|||||||
|
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header"><i class="fa fa-shopping-cart"></i> {% trans 'Shopping Recipes' %}</div>
|
||||||
<i class="fa fa-shopping-cart"></i> {% trans 'Shopping Recipes' %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<template v-if="shopping_list.recipes.length < 1">
|
<template v-if="shopping_list.recipes.length < 1"> {% trans 'No recipes selected' %} </template>
|
||||||
{% trans 'No recipes selected' %}
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="row flex-row my-auto" v-for="x in shopping_list.recipes"
|
<div class="row flex-row my-auto" v-for="x in shopping_list.recipes" style="margin-top: 1vh !important">
|
||||||
style="margin-top: 1vh!important;">
|
|
||||||
<div class="flex-column align-self-start" style="margin-right: 0.4vw">
|
<div class="flex-column align-self-start" style="margin-right: 0.4vw">
|
||||||
<button class="btn btn-outline-danger" @click="removeRecipeFromList(x)"><i
|
<button class="btn btn-outline-danger" @click="removeRecipeFromList(x)"><i class="fa fa-trash"></i></button>
|
||||||
class="fa fa-trash"></i></button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow-1 flex-column my-auto"><a v-bind:href="getRecipeUrl(x.recipe)"
|
<div class="flex-grow-1 flex-column my-auto">
|
||||||
target="_blank"
|
<a v-bind:href="getRecipeUrl(x.recipe)" target="_blank" rel="nofollow norefferer">[[x.recipe_name]]</a>
|
||||||
rel="nofollow norefferer">[[x.recipe_name]]</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-column align-self-end">
|
<div class="flex-column align-self-end">
|
||||||
<div class="input-group input-group-sm my-auto">
|
<div class="input-group input-group-sm my-auto">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<button class="text-muted btn btn-outline-primary shadow-none"
|
<button class="text-muted btn btn-outline-primary shadow-none" @click="((x.servings - 1) > 0) ? x.servings -= 1 : 1">-</button>
|
||||||
@click="((x.servings - 1) > 0) ? x.servings -= 1 : 1">-
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<input class="form-control" type="number" v-model="x.servings">
|
<input class="form-control" type="number" v-model="x.servings" />
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button class="text-muted btn btn-outline-primary shadow-none"
|
<button class="text-muted btn btn-outline-primary shadow-none" @click="x.servings += 1">+</button>
|
||||||
@click="x.servings += 1">
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -118,61 +104,57 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table table-sm" style="margin-top: 1vh">
|
<table class="table table-sm" style="margin-top: 1vh">
|
||||||
|
|
||||||
<template v-for="c in display_categories">
|
<template v-for="c in display_categories">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="5">[[c.name]]</th>
|
<th colspan="5">[[c.name]]</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody is="draggable" :list="c.entries" tag="tbody" group="people" @sort="sortEntries"
|
<tbody is="draggable" :list="c.entries" tag="tbody" group="people" @sort="sortEntries" @change="dragChanged(c, $event)" handle=".handle">
|
||||||
@change="dragChanged(c, $event)" handle=".handle">
|
<tr v-for="(element, index) in c.entries" :key="element.id" v-bind:class="{ 'text-muted': element.checked }">
|
||||||
<tr v-for="(element, index) in c.entries" :key="element.id"
|
|
||||||
v-bind:class="{ 'text-muted': element.checked }">
|
|
||||||
<td class="handle"><i class="fas fa-sort"></i></td>
|
<td class="handle"><i class="fas fa-sort"></i></td>
|
||||||
<td>[[element.amount.toFixed(2)]]</td>
|
<td>[[element.amount.toFixed(2)]]</td>
|
||||||
<td>[[element.unit.name]]</td>
|
<td>[[element.unit.name]]</td>
|
||||||
<td>[[element.food.name]]</td>
|
<td>[[element.food.name]]</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-outline-danger" v-if="element.list_recipe === null"
|
<button
|
||||||
@click="shopping_list.entries = shopping_list.entries.filter(item => item.id !== element.id)">
|
class="btn btn-sm btn-outline-danger"
|
||||||
<i class="fa fa-trash"></i></button>
|
v-if="element.list_recipe === null"
|
||||||
|
@click="shopping_list.entries = shopping_list.entries.filter(item => item.id !== element.id)"
|
||||||
|
>
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="row" style="text-align: right">
|
<div class="row" style="text-align: right">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<b-form-checkbox switch v-model="entry_mode_simple"
|
<b-form-checkbox switch v-model="entry_mode_simple" @change="$cookies.set('shopping_entry_mode_simple',!entry_mode_simple, -1)"
|
||||||
@change="$cookies.set('shopping_entry_mode_simple',!entry_mode_simple, -1)">{% trans 'Entry Mode' %}</b-form-checkbox>
|
>{% trans 'Entry Mode' %}</b-form-checkbox
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" v-if="entry_mode_simple" style="margin-top: 2vh">
|
<div class="row" v-if="entry_mode_simple" style="margin-top: 2vh">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<form v-on:submit.prevent="addSimpleEntry()">
|
<form v-on:submit.prevent="addSimpleEntry()">
|
||||||
|
|
||||||
<label for="id_simple_entry">{% trans 'Add Entry' %}</label>
|
<label for="id_simple_entry">{% trans 'Add Entry' %}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input id="id_simple_entry" class="form-control" v-model="simple_entry">
|
<input id="id_simple_entry" class="form-control" v-model="simple_entry" />
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button class="btn btn-outline-secondary" type="button" @click="addSimpleEntry()"><i
|
<button class="btn btn-outline-secondary" type="button" @click="addSimpleEntry()"><i class="fa fa-plus"></i></button>
|
||||||
class="fa fa-plus"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="row" v-if="!entry_mode_simple" style="margin-top: 2vh">
|
<div class="row" v-if="!entry_mode_simple" style="margin-top: 2vh">
|
||||||
<div class="col-12 col-lg-3">
|
<div class="col-12 col-lg-3">
|
||||||
<input id="id_advanced_entry" class="form-control" type="number" placeholder="{% trans 'Amount' %}"
|
<input id="id_advanced_entry" class="form-control" type="number" placeholder="{% trans 'Amount' %}" v-model="new_entry.amount" ref="new_entry_amount" />
|
||||||
v-model="new_entry.amount" ref="new_entry_amount">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-4">
|
<div class="col-12 col-lg-4">
|
||||||
<multiselect
|
<multiselect
|
||||||
@ -193,7 +175,8 @@
|
|||||||
track-by="name"
|
track-by="name"
|
||||||
:multiple="false"
|
:multiple="false"
|
||||||
:loading="units_loading"
|
:loading="units_loading"
|
||||||
@search-change="searchUnits">
|
@search-change="searchUnits"
|
||||||
|
>
|
||||||
</multiselect>
|
</multiselect>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-lg-4">
|
<div class="col-12 col-lg-4">
|
||||||
@ -215,13 +198,13 @@
|
|||||||
track-by="name"
|
track-by="name"
|
||||||
:multiple="false"
|
:multiple="false"
|
||||||
:loading="foods_loading"
|
:loading="foods_loading"
|
||||||
@search-change="searchFoods">
|
@search-change="searchFoods"
|
||||||
|
>
|
||||||
</multiselect>
|
</multiselect>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-lg-1 my-auto text-right">
|
<div class="col-12 col-lg-1 my-auto text-right">
|
||||||
<button class="btn btn-success btn-lg" @click="addEntry()"><i class="fa fa-plus"></i>
|
<button class="btn btn-success btn-lg" @click="addEntry()"><i class="fa fa-plus"></i></button>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -243,9 +226,9 @@
|
|||||||
track-by="id"
|
track-by="id"
|
||||||
:multiple="false"
|
:multiple="false"
|
||||||
:loading="supermarkets_loading"
|
:loading="supermarkets_loading"
|
||||||
@search-change="searchSupermarket">
|
@search-change="searchSupermarket"
|
||||||
|
>
|
||||||
</multiselect>
|
</multiselect>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -267,34 +250,26 @@
|
|||||||
track-by="id"
|
track-by="id"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:loading="users_loading"
|
:loading="users_loading"
|
||||||
@search-change="searchUsers">
|
@search-change="searchUsers"
|
||||||
|
>
|
||||||
</multiselect>
|
</multiselect>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col" style="text-align: right; margin-top: 1vh">
|
<div class="col" style="text-align: right; margin-top: 1vh">
|
||||||
|
|
||||||
<div class="form-group form-check form-group-lg">
|
<div class="form-group form-check form-group-lg">
|
||||||
<input class="form-check-input" style="zoom:1.3;" type="checkbox"
|
<input class="form-check-input" style="zoom: 1.3" type="checkbox" v-model="shopping_list.finished" id="id_finished" />
|
||||||
v-model="shopping_list.finished" id="id_finished">
|
<label class="form-check-label" style="zoom: 1.3" for="id_finished"> {% trans 'Finished' %}</label>
|
||||||
<label class="form-check-label" style="zoom:1.3;"
|
</div>
|
||||||
for="id_finished"> {% trans 'Finished' %}</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
|
||||||
{% if request.user.userpreference.shopping_auto_sync > 0 %}
|
{% if request.user.userpreference.shopping_auto_sync > 0 %}
|
||||||
<div class="row" v-if="!onLine">
|
<div class="row" v-if="!onLine">
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<div class="alert alert-warning" role="alert">
|
<div class="alert alert-warning" role="alert">{% trans 'You are offline, shopping list might not syncronize.' %}</div>
|
||||||
{% trans 'You are offline, shopping list might not syncronize.' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -309,13 +284,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr v-for="x in c.entries">
|
<tr v-for="x in c.entries">
|
||||||
<template v-if="!x.checked">
|
<template v-if="!x.checked">
|
||||||
<td><input type="checkbox" style="zoom:1.4;" v-model="x.checked"
|
<td><input type="checkbox" style="zoom: 1.4" v-model="x.checked" @change="entryChecked(x)" /></td>
|
||||||
@change="entryChecked(x)">
|
|
||||||
</td>
|
|
||||||
<td>[[x.amount.toFixed(2)]]</td>
|
<td>[[x.amount.toFixed(2)]]</td>
|
||||||
<td>[[x.unit.name]]</td>
|
<td>[[x.unit.name]]</td>
|
||||||
<td>[[x.food.name]] <span class="text-muted" v-if="x.recipes.length > 0">([[x.recipes.join(', ')]])</span>
|
<td>[[x.food.name]] <span class="text-muted" v-if="x.recipes.length > 0">([[x.recipes.join(', ')]])</span></td>
|
||||||
</td>
|
|
||||||
</template>
|
</template>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@ -324,34 +296,24 @@
|
|||||||
<td colspan="4"></td>
|
<td colspan="4"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="c in display_categories">
|
<template v-for="c in display_categories">
|
||||||
|
|
||||||
<tr v-for="x in c.entries" class="text-muted">
|
<tr v-for="x in c.entries" class="text-muted">
|
||||||
<template v-if="x.checked">
|
<template v-if="x.checked">
|
||||||
<td><input type="checkbox" style="zoom:1.4;" v-model="x.checked"
|
<td><input type="checkbox" style="zoom: 1.4" v-model="x.checked" @change="entryChecked(x)" /></td>
|
||||||
@change="entryChecked(x)">
|
|
||||||
</td>
|
|
||||||
<td>[[x.amount]]</td>
|
<td>[[x.amount]]</td>
|
||||||
<td>[[x.unit.name]]</td>
|
<td>[[x.unit.name]]</td>
|
||||||
<td>[[x.food.name]]</td>
|
<td>[[x.food.name]]</td>
|
||||||
</template>
|
</template>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" style="margin-top: 2vh">
|
<div class="row" style="margin-top: 2vh">
|
||||||
<div class="col" style="text-align: right">
|
<div class="col" style="text-align: right">
|
||||||
<b-button class="btn btn-info" v-b-modal.id_modal_export><i
|
<b-button class="btn btn-info" v-b-modal.id_modal_export><i class="fas fa-file-export"></i> {% trans 'Export' %}</b-button>
|
||||||
class="fas fa-file-export"></i> {% trans 'Export' %}</b-button>
|
<button class="btn btn-success" @click="updateShoppingList()" v-if="edit_mode"><i class="fas fa-save"></i> {% trans 'Save' %}</button>
|
||||||
<button class="btn btn-success" @click="updateShoppingList()" v-if="edit_mode"><i
|
|
||||||
class="fas fa-save"></i> {% trans 'Save' %}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -363,28 +325,20 @@
|
|||||||
<div class="col col-12">
|
<div class="col col-12">
|
||||||
<label>
|
<label>
|
||||||
{% trans 'List Prefix' %}
|
{% trans 'List Prefix' %}
|
||||||
<input class="form-control" v-model="export_text_prefix">
|
<input class="form-control" v-model="export_text_prefix" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-12">
|
<div class="col col-12">
|
||||||
<b-form-textarea class="form-control" max-rows="8" v-model="export_text">
|
<b-form-textarea class="form-control" max-rows="8" v-model="export_text"> </b-form-textarea>
|
||||||
|
|
||||||
</b-form-textarea>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</b-modal>
|
</b-modal>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
{% endblock %} {% block script %}
|
||||||
{% endblock %}
|
|
||||||
{% block script %}
|
|
||||||
|
|
||||||
<script src="{% url 'javascript-catalog' %}"></script>
|
<script src="{% url 'javascript-catalog' %}"></script>
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
@ -613,7 +567,6 @@
|
|||||||
this.onLine = type === 'online';
|
this.onLine = type === 'online';
|
||||||
},
|
},
|
||||||
makeToast: function (title, message, variant = null) {
|
makeToast: function (title, message, variant = null) {
|
||||||
//TODO remove duplicate function in favor of central one
|
|
||||||
this.$bvToast.toast(message, {
|
this.$bvToast.toast(message, {
|
||||||
title: title,
|
title: title,
|
||||||
variant: variant,
|
variant: variant,
|
||||||
@ -622,7 +575,7 @@
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
loadInitialRecipe: function (recipe, servings) {
|
loadInitialRecipe: function (recipe, servings) {
|
||||||
servings = 1 //TODO temporary until i can actually fix the servings for this #453
|
servings = 1
|
||||||
return this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe)).then((response) => {
|
return this.$http.get('{% url 'api:recipe-detail' 123456 %}'.replace('123456', recipe)).then((response) => {
|
||||||
this.addRecipeToList(response.data, servings)
|
this.addRecipeToList(response.data, servings)
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
@ -735,7 +688,7 @@
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
sortEntries: function (a, b) {
|
sortEntries: function (a, b) {
|
||||||
//TODO implement me (might be difficult because of computed drag changed stuff)
|
|
||||||
},
|
},
|
||||||
dragChanged: function (category, evt) {
|
dragChanged: function (category, evt) {
|
||||||
if (evt.added !== undefined) {
|
if (evt.added !== undefined) {
|
||||||
@ -871,7 +824,7 @@
|
|||||||
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
|
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getRecipeUrl: function (id) { //TODO generic function that can be reused else were
|
getRecipeUrl: function (id) {
|
||||||
return '{% url 'view_recipe' 123456 %}'.replace('123456', id)
|
return '{% url 'view_recipe' 123456 %}'.replace('123456', id)
|
||||||
},
|
},
|
||||||
addRecipeToList: function (recipe, servings = 1) {
|
addRecipeToList: function (recipe, servings = 1) {
|
||||||
@ -917,7 +870,7 @@
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
searchUnits: function (query) { //TODO move to central component
|
searchUnits: function (query) {
|
||||||
this.units_loading = true
|
this.units_loading = true
|
||||||
this.$http.get("{% url 'api:unit-list' %}" + '?query=' + query + '&limit=10').then((response) => {
|
this.$http.get("{% url 'api:unit-list' %}" + '?query=' + query + '&limit=10').then((response) => {
|
||||||
this.units = response.data.results;
|
this.units = response.data.results;
|
||||||
@ -926,7 +879,7 @@
|
|||||||
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
|
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
searchFoods: function (query) { //TODO move to central component
|
searchFoods: function (query) {
|
||||||
this.foods_loading = true
|
this.foods_loading = true
|
||||||
this.$http.get("{% url 'api:food-list' %}" + '?query=' + query + '&limit=10').then((response) => {
|
this.$http.get("{% url 'api:food-list' %}" + '?query=' + query + '&limit=10').then((response) => {
|
||||||
this.foods = response.data.results
|
this.foods = response.data.results
|
||||||
@ -935,17 +888,17 @@
|
|||||||
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
|
this.makeToast(gettext('Error'), gettext("There was an error loading a resource!") + err.bodyText, 'danger')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addFoodType: function (tag, index) { //TODO move to central component
|
addFoodType: function (tag, index) {
|
||||||
let new_food = {'name': tag, supermarket_category: null}
|
let new_food = {'name': tag, supermarket_category: null}
|
||||||
this.foods.push(new_food)
|
this.foods.push(new_food)
|
||||||
this.new_entry.food = new_food
|
this.new_entry.food = new_food
|
||||||
},
|
},
|
||||||
addUnitType: function (tag, index) { //TODO move to central component
|
addUnitType: function (tag, index) {
|
||||||
let new_unit = {'name': tag}
|
let new_unit = {'name': tag}
|
||||||
this.units.push(new_unit)
|
this.units.push(new_unit)
|
||||||
this.new_entry.unit = new_unit
|
this.new_entry.unit = new_unit
|
||||||
},
|
},
|
||||||
searchUsers: function (query) { //TODO move to central component
|
searchUsers: function (query) {
|
||||||
this.users_loading = true
|
this.users_loading = true
|
||||||
this.$http.get("{% url 'api:username-list' %}" + '?query=' + query + '&limit=10').then((response) => {
|
this.$http.get("{% url 'api:username-list' %}" + '?query=' + query + '&limit=10').then((response) => {
|
||||||
this.users = response.data
|
this.users = response.data
|
||||||
|
@ -387,6 +387,7 @@ def user_settings(request):
|
|||||||
up.shopping_auto_sync = shopping_form.cleaned_data['shopping_auto_sync']
|
up.shopping_auto_sync = shopping_form.cleaned_data['shopping_auto_sync']
|
||||||
up.filter_to_supermarket = shopping_form.cleaned_data['filter_to_supermarket']
|
up.filter_to_supermarket = shopping_form.cleaned_data['filter_to_supermarket']
|
||||||
up.default_delay = shopping_form.cleaned_data['default_delay']
|
up.default_delay = shopping_form.cleaned_data['default_delay']
|
||||||
|
up.shopping_recent_days = shopping_form.cleaned_data['shopping_recent_days']
|
||||||
if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL:
|
if up.shopping_auto_sync < settings.SHOPPING_MIN_AUTOSYNC_INTERVAL:
|
||||||
up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL
|
up.shopping_auto_sync = settings.SHOPPING_MIN_AUTOSYNC_INTERVAL
|
||||||
up.save()
|
up.save()
|
||||||
|
@ -25,24 +25,10 @@
|
|||||||
<b-form-input min="1" type="number" :description="$t('Amount')" v-model="new_item.amount"></b-form-input>
|
<b-form-input min="1" type="number" :description="$t('Amount')" v-model="new_item.amount"></b-form-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-3">
|
<div class="col col-md-3">
|
||||||
<generic-multiselect
|
<lookup-input :form="formUnit" :model="Models.UNIT" @change="new_item.unit = $event" :show_label="false" />
|
||||||
@change="new_item.unit = $event.val"
|
|
||||||
:model="Models.UNIT"
|
|
||||||
:multiple="false"
|
|
||||||
:allow_create="false"
|
|
||||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
|
||||||
:placeholder="$t('Unit')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-4">
|
<div class="col col-md-4">
|
||||||
<generic-multiselect
|
<lookup-input :form="formFood" :model="Models.FOOD" @change="new_item.food = $event" :show_label="false" />
|
||||||
@change="new_item.food = $event.val"
|
|
||||||
:model="Models.FOOD"
|
|
||||||
:multiple="false"
|
|
||||||
:allow_create="false"
|
|
||||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
|
||||||
:placeholder="$t('Food')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-1 ">
|
<div class="col col-md-1 ">
|
||||||
<b-button variant="link" class="px-0">
|
<b-button variant="link" class="px-0">
|
||||||
@ -107,7 +93,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</b-tab>
|
</b-tab>
|
||||||
<!-- settings tab -->
|
<!-- supermarkets tab -->
|
||||||
<b-tab :title="$t('Supermarkets')">
|
<b-tab :title="$t('Supermarkets')">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col col-md-5">
|
<div class="col col-md-5">
|
||||||
@ -183,9 +169,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</b-card>
|
</b-card>
|
||||||
|
|
||||||
<b-card-sub-title v-if="new_supermarket.editmode" class="pt-0 pb-3"
|
<b-card-sub-title v-if="new_supermarket.editmode" class="pt-0 pb-3">{{ $t("CategoryInstruction") }}</b-card-sub-title>
|
||||||
>Drag categories to change the order categories appear in shopping list.</b-card-sub-title
|
|
||||||
>
|
|
||||||
<b-card
|
<b-card
|
||||||
v-if="new_supermarket.editmode && supermarketCategory.length === 0"
|
v-if="new_supermarket.editmode && supermarketCategory.length === 0"
|
||||||
class="m-0 p-0 font-weight-bold no-body"
|
class="m-0 p-0 font-weight-bold no-body"
|
||||||
@ -328,6 +312,20 @@
|
|||||||
</em>
|
</em>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-6">{{ $t("shopping_recent_days") }}</div>
|
||||||
|
<div class="col col-md-6 text-right">
|
||||||
|
<input type="number" size="sm" v-model="settings.shopping_recent_days" @change="saveSettings" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row sm mb-3">
|
||||||
|
<div class="col">
|
||||||
|
<em class="small text-muted">
|
||||||
|
{{ $t("shopping_recent_days_desc") }}
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">{{ $t("filter_to_supermarket") }}</div>
|
<div class="col col-md-6">{{ $t("filter_to_supermarket") }}</div>
|
||||||
<div class="col col-md-6 text-right">
|
<div class="col col-md-6 text-right">
|
||||||
@ -459,6 +457,7 @@ import ContextMenuItem from "@/components/ContextMenu/ContextMenuItem"
|
|||||||
import ShoppingLineItem from "@/components/ShoppingLineItem"
|
import ShoppingLineItem from "@/components/ShoppingLineItem"
|
||||||
import GenericMultiselect from "@/components/GenericMultiselect"
|
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||||
import GenericPill from "@/components/GenericPill"
|
import GenericPill from "@/components/GenericPill"
|
||||||
|
import LookupInput from "@/components/Modals/LookupInput"
|
||||||
import draggable from "vuedraggable"
|
import draggable from "vuedraggable"
|
||||||
|
|
||||||
import { ApiMixin, getUserPreference } from "@/utils/utils"
|
import { ApiMixin, getUserPreference } from "@/utils/utils"
|
||||||
@ -470,7 +469,7 @@ Vue.use(BootstrapVue)
|
|||||||
export default {
|
export default {
|
||||||
name: "ShoppingListView",
|
name: "ShoppingListView",
|
||||||
mixins: [ApiMixin],
|
mixins: [ApiMixin],
|
||||||
components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect, GenericPill, draggable },
|
components: { ContextMenu, ContextMenuItem, ShoppingLineItem, GenericMultiselect, GenericPill, draggable, LookupInput },
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -492,6 +491,7 @@ export default {
|
|||||||
mealplan_autoinclude_related: false,
|
mealplan_autoinclude_related: false,
|
||||||
mealplan_autoexclude_onhand: true,
|
mealplan_autoexclude_onhand: true,
|
||||||
filter_to_supermarket: false,
|
filter_to_supermarket: false,
|
||||||
|
shopping_recent_days: 7,
|
||||||
},
|
},
|
||||||
new_supermarket: { entrymode: false, value: undefined, editmode: undefined },
|
new_supermarket: { entrymode: false, value: undefined, editmode: undefined },
|
||||||
new_category: { entrymode: false, value: undefined },
|
new_category: { entrymode: false, value: undefined },
|
||||||
@ -577,6 +577,16 @@ export default {
|
|||||||
defaultDelay() {
|
defaultDelay() {
|
||||||
return getUserPreference("default_delay") || 2
|
return getUserPreference("default_delay") || 2
|
||||||
},
|
},
|
||||||
|
formUnit() {
|
||||||
|
let unit = this.Models.SHOPPING_LIST.create.form.unit
|
||||||
|
unit.value = this.new_item.unit
|
||||||
|
return unit
|
||||||
|
},
|
||||||
|
formFood() {
|
||||||
|
let food = this.Models.SHOPPING_LIST.create.form.food
|
||||||
|
food.value = this.new_item.food
|
||||||
|
return food
|
||||||
|
},
|
||||||
itemsDelayed() {
|
itemsDelayed() {
|
||||||
return this.items.filter((x) => !x.delay_until || !Date.parse(x?.delay_until) > new Date(Date.now())).length < this.items.length
|
return this.items.filter((x) => !x.delay_until || !Date.parse(x?.delay_until) > new Date(Date.now())).length < this.items.length
|
||||||
},
|
},
|
||||||
@ -647,6 +657,10 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// this.genericAPI inherited from ApiMixin
|
// this.genericAPI inherited from ApiMixin
|
||||||
|
test(e) {
|
||||||
|
this.new_item.unit = e
|
||||||
|
console.log(e, this.new_item, this.formUnit)
|
||||||
|
},
|
||||||
addItem() {
|
addItem() {
|
||||||
let api = new ApiApiFactory()
|
let api = new ApiApiFactory()
|
||||||
api.createShoppingListEntry(this.new_item)
|
api.createShoppingListEntry(this.new_item)
|
||||||
|
@ -1,53 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<!-- TODO: Deprecate -->
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
||||||
<div class="col col-md-12">
|
<div class="col col-md-12">
|
||||||
<h2>{{ $t('Supermarket') }}</h2>
|
<h2>{{ $t("Supermarket") }}</h2>
|
||||||
|
|
||||||
<multiselect v-model="selected_supermarket" track-by="id" label="name"
|
<multiselect v-model="selected_supermarket" track-by="id" label="name" :options="supermarkets" @input="selectedSupermarketChanged"> </multiselect>
|
||||||
:options="supermarkets" @input="selectedSupermarketChanged">
|
|
||||||
</multiselect>
|
|
||||||
|
|
||||||
<b-button class="btn btn-primary btn-block" style="margin-top: 1vh" v-b-modal.modal-supermarket>
|
<b-button class="btn btn-primary btn-block" style="margin-top: 1vh" v-b-modal.modal-supermarket>
|
||||||
{{ $t('Edit') }}
|
{{ $t("Edit") }}
|
||||||
</b-button>
|
|
||||||
<b-button class="btn btn-success btn-block" @click="selected_supermarket = {new:true, name:''}"
|
|
||||||
v-b-modal.modal-supermarket>{{ $t('New') }}
|
|
||||||
</b-button>
|
</b-button>
|
||||||
|
<b-button class="btn btn-success btn-block" @click="selected_supermarket = { new: true, name: '' }" v-b-modal.modal-supermarket>{{ $t("New") }} </b-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr>
|
<hr />
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<h4>{{ $t('Categories') }}
|
<h4>
|
||||||
<button class="btn btn-success btn-sm" @click="selected_category = {new:true, name:''}"
|
{{ $t("Categories") }}
|
||||||
v-b-modal.modal-category>{{ $t('New') }}
|
<button class="btn btn-success btn-sm" @click="selected_category = { new: true, name: '' }" v-b-modal.modal-category>{{ $t("New") }}</button>
|
||||||
</button>
|
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<draggable :list="selectable_categories" group="supermarket_categories"
|
<draggable :list="selectable_categories" group="supermarket_categories" :empty-insert-threshold="10">
|
||||||
:empty-insert-threshold="10">
|
|
||||||
<div v-for="c in selectable_categories" :key="c.id">
|
<div v-for="c in selectable_categories" :key="c.id">
|
||||||
<button class="btn btn-block btn-sm btn-primary" style="margin-top: 0.5vh">{{ c.name }}</button>
|
<button class="btn btn-block btn-sm btn-primary" style="margin-top: 0.5vh">{{ c.name }}</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</draggable>
|
</draggable>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<h4>{{ $t('Selected') }} {{ $t('Categories') }}</h4>
|
<h4>{{ $t("Selected") }} {{ $t("Categories") }}</h4>
|
||||||
|
|
||||||
<draggable :list="supermarket_categories" group="supermarket_categories"
|
<draggable :list="supermarket_categories" group="supermarket_categories" :empty-insert-threshold="10" @change="selectedCategoriesChanged">
|
||||||
:empty-insert-threshold="10" @change="selectedCategoriesChanged">
|
|
||||||
<div v-for="c in supermarket_categories" :key="c.id">
|
<div v-for="c in supermarket_categories" :key="c.id">
|
||||||
<button class="btn btn-block btn-sm btn-primary" style="margin-top: 0.5vh">{{ c.name }}</button>
|
<button class="btn btn-block btn-sm btn-primary" style="margin-top: 0.5vh">{{ c.name }}</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</draggable>
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
@ -56,52 +43,45 @@
|
|||||||
<!-- EDIT MODALS -->
|
<!-- EDIT MODALS -->
|
||||||
<b-modal id="modal-supermarket" v-bind:title="$t('Supermarket')" @ok="supermarketModalOk()">
|
<b-modal id="modal-supermarket" v-bind:title="$t('Supermarket')" @ok="supermarketModalOk()">
|
||||||
<label v-if="selected_supermarket !== undefined">
|
<label v-if="selected_supermarket !== undefined">
|
||||||
{{ $t('Name') }}
|
{{ $t("Name") }}
|
||||||
<b-input v-model="selected_supermarket.name"></b-input>
|
<b-input v-model="selected_supermarket.name"></b-input>
|
||||||
|
|
||||||
</label>
|
</label>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
|
|
||||||
<b-modal id="modal-category" v-bind:title="$t('Category')" @ok="categoryModalOk()">
|
<b-modal id="modal-category" v-bind:title="$t('Category')" @ok="categoryModalOk()">
|
||||||
<label v-if="selected_category !== undefined">
|
<label v-if="selected_category !== undefined">
|
||||||
{{ $t('Name') }}
|
{{ $t("Name") }}
|
||||||
<b-input v-model="selected_category.name"></b-input>
|
<b-input v-model="selected_category.name"></b-input>
|
||||||
|
|
||||||
</label>
|
</label>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Vue from 'vue'
|
import Vue from "vue"
|
||||||
import {BootstrapVue} from 'bootstrap-vue'
|
import { BootstrapVue } from "bootstrap-vue"
|
||||||
|
|
||||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||||
|
|
||||||
import {ResolveUrlMixin, ToastMixin} from "@/utils/utils";
|
import { ResolveUrlMixin, ToastMixin } from "@/utils/utils"
|
||||||
|
|
||||||
|
import { ApiApiFactory } from "@/utils/openapi/api.ts"
|
||||||
import {ApiApiFactory} from "@/utils/openapi/api.ts";
|
|
||||||
|
|
||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
import draggable from 'vuedraggable'
|
import draggable from "vuedraggable"
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from "axios"
|
||||||
import Multiselect from "vue-multiselect";
|
import Multiselect from "vue-multiselect"
|
||||||
|
|
||||||
axios.defaults.xsrfHeaderName = 'X-CSRFToken'
|
axios.defaults.xsrfHeaderName = "X-CSRFToken"
|
||||||
axios.defaults.xsrfCookieName = 'csrftoken'
|
axios.defaults.xsrfCookieName = "csrftoken"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SupermarketView',
|
name: "SupermarketView",
|
||||||
mixins: [
|
mixins: [ResolveUrlMixin, ToastMixin],
|
||||||
ResolveUrlMixin,
|
|
||||||
ToastMixin,
|
|
||||||
],
|
|
||||||
components: {
|
components: {
|
||||||
Multiselect,
|
Multiselect,
|
||||||
draggable
|
draggable,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -111,7 +91,6 @@ export default {
|
|||||||
selected_supermarket: {},
|
selected_supermarket: {},
|
||||||
selected_category: {},
|
selected_category: {},
|
||||||
|
|
||||||
|
|
||||||
selectable_categories: [],
|
selectable_categories: [],
|
||||||
supermarket_categories: [],
|
supermarket_categories: [],
|
||||||
}
|
}
|
||||||
@ -123,10 +102,10 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
loadInitial: function() {
|
loadInitial: function() {
|
||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
apiClient.listSupermarkets().then(results => {
|
apiClient.listSupermarkets().then((results) => {
|
||||||
this.supermarkets = results.data
|
this.supermarkets = results.data
|
||||||
})
|
})
|
||||||
apiClient.listSupermarketCategorys().then(results => {
|
apiClient.listSupermarketCategorys().then((results) => {
|
||||||
this.categories = results.data
|
this.categories = results.data
|
||||||
this.selectable_categories = this.categories
|
this.selectable_categories = this.categories
|
||||||
})
|
})
|
||||||
@ -134,21 +113,24 @@ export default {
|
|||||||
selectedCategoriesChanged: function(data) {
|
selectedCategoriesChanged: function(data) {
|
||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
|
|
||||||
if ('removed' in data) {
|
if ("removed" in data) {
|
||||||
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === data.removed.element.id)[0]
|
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === data.removed.element.id)[0]
|
||||||
apiClient.destroySupermarketCategoryRelation(relation.id)
|
apiClient.destroySupermarketCategoryRelation(relation.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('added' in data) {
|
if ("added" in data) {
|
||||||
apiClient.createSupermarketCategoryRelation({
|
apiClient
|
||||||
|
.createSupermarketCategoryRelation({
|
||||||
category: data.added.element,
|
category: data.added.element,
|
||||||
supermarket: this.selected_supermarket.id, order: 0
|
supermarket: this.selected_supermarket.id,
|
||||||
}).then(results => {
|
order: 0,
|
||||||
|
})
|
||||||
|
.then((results) => {
|
||||||
this.selected_supermarket.category_to_supermarket.push(results.data)
|
this.selected_supermarket.category_to_supermarket.push(results.data)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('moved' in data || 'added' in data) {
|
if ("moved" in data || "added" in data) {
|
||||||
this.supermarket_categories.forEach((element, index) => {
|
this.supermarket_categories.forEach((element, index) => {
|
||||||
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === element.id)[0]
|
let relation = this.selected_supermarket.category_to_supermarket.filter((el) => el.category.id === element.id)[0]
|
||||||
console.log(relation)
|
console.log(relation)
|
||||||
@ -164,38 +146,33 @@ export default {
|
|||||||
this.supermarket_categories.push(i.category)
|
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
|
return el.id !== i.category.id
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
supermarketModalOk: function() {
|
supermarketModalOk: function() {
|
||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
if (this.selected_supermarket.new) {
|
if (this.selected_supermarket.new) {
|
||||||
apiClient.createSupermarket({name: this.selected_supermarket.name}).then(results => {
|
apiClient.createSupermarket({ name: this.selected_supermarket.name }).then((results) => {
|
||||||
this.selected_supermarket = undefined
|
this.selected_supermarket = undefined
|
||||||
this.loadInitial()
|
this.loadInitial()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
apiClient.partialUpdateSupermarket(this.selected_supermarket.id, { name: this.selected_supermarket.name })
|
apiClient.partialUpdateSupermarket(this.selected_supermarket.id, { name: this.selected_supermarket.name })
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
categoryModalOk: function() {
|
categoryModalOk: function() {
|
||||||
let apiClient = new ApiApiFactory()
|
let apiClient = new ApiApiFactory()
|
||||||
if (this.selected_category.new) {
|
if (this.selected_category.new) {
|
||||||
apiClient.createSupermarketCategory({name: this.selected_category.name}).then(results => {
|
apiClient.createSupermarketCategory({ name: this.selected_category.name }).then((results) => {
|
||||||
this.selected_category = {}
|
this.selected_category = {}
|
||||||
this.loadInitial()
|
this.loadInitial()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
apiClient.partialUpdateSupermarketCategory(this.selected_category.id, { name: this.selected_category.name })
|
apiClient.partialUpdateSupermarketCategory(this.selected_category.id, { name: this.selected_category.name })
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style></style>
|
||||||
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
<p v-if="f.type == 'instruction'">{{ f.label }}</p>
|
<p v-if="f.type == 'instruction'">{{ f.label }}</p>
|
||||||
<!-- this lookup is single selection -->
|
<!-- this lookup is single selection -->
|
||||||
<lookup-input v-if="f.type == 'lookup'" :form="f" :model="listModel(f.list)" @change="storeValue" />
|
<lookup-input v-if="f.type == 'lookup'" :form="f" :model="listModel(f.list)" @change="storeValue" />
|
||||||
<!-- TODO add ability to create new items associated with lookup -->
|
|
||||||
<!-- TODO: add multi-selection input list -->
|
<!-- TODO: add multi-selection input list -->
|
||||||
<checkbox-input v-if="f.type == 'checkbox'" :label="f.label" :value="f.value" :field="f.field" />
|
<checkbox-input v-if="f.type == 'checkbox'" :label="f.label" :value="f.value" :field="f.field" />
|
||||||
<text-input v-if="f.type == 'text'" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" />
|
<text-input v-if="f.type == 'text'" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" />
|
||||||
|
@ -238,7 +238,6 @@
|
|||||||
"mealplan_autoinclude_related_desc": "When adding a meal plan to the shopping list (manually or automatically), include all related recipes.",
|
"mealplan_autoinclude_related_desc": "When adding a meal plan to the shopping list (manually or automatically), include all related recipes.",
|
||||||
"default_delay_desc": "Default number of hours to delay a shopping list entry.",
|
"default_delay_desc": "Default number of hours to delay a shopping list entry.",
|
||||||
"filter_to_supermarket": "Filter to Supermarket",
|
"filter_to_supermarket": "Filter to Supermarket",
|
||||||
"filter_to_supermarket_desc": "Filter shopping list to only include supermarket categories.",
|
|
||||||
"Week_Numbers": "Week numbers",
|
"Week_Numbers": "Week numbers",
|
||||||
"Show_Week_Numbers": "Show week numbers ?",
|
"Show_Week_Numbers": "Show week numbers ?",
|
||||||
"Export_As_ICal": "Export current period to iCal format",
|
"Export_As_ICal": "Export current period to iCal format",
|
||||||
@ -260,6 +259,10 @@
|
|||||||
"nothing": "Nothing to do",
|
"nothing": "Nothing to do",
|
||||||
"err_merge_self": "Cannot merge item with itself",
|
"err_merge_self": "Cannot merge item with itself",
|
||||||
"show_sql": "Show SQL",
|
"show_sql": "Show SQL",
|
||||||
|
"filter_to_supermarket_desc": "By default, filter shopping list to only include categories for selected supermarket.",
|
||||||
"CategoryName": "Category Name",
|
"CategoryName": "Category Name",
|
||||||
"SupermarketName": "Supermarket Name"
|
"SupermarketName": "Supermarket Name",
|
||||||
|
"CategoryInstruction": "Drag categories to change the order categories appear in shopping list.",
|
||||||
|
"shopping_recent_days_desc": "Days of recent shopping list entries to display.",
|
||||||
|
"shopping_recent_days": "Recent Days"
|
||||||
}
|
}
|
||||||
|
@ -216,6 +216,24 @@ export class Models {
|
|||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
params: [["amount", "unit", "food", "checked"]],
|
params: [["amount", "unit", "food", "checked"]],
|
||||||
|
form: {
|
||||||
|
unit: {
|
||||||
|
form_field: true,
|
||||||
|
type: "lookup",
|
||||||
|
field: "unit",
|
||||||
|
list: "UNIT",
|
||||||
|
label: i18n.t("Unit"),
|
||||||
|
allow_create: true,
|
||||||
|
},
|
||||||
|
food: {
|
||||||
|
form_field: true,
|
||||||
|
type: "lookup",
|
||||||
|
field: "food",
|
||||||
|
list: "FOOD",
|
||||||
|
label: i18n.t("Food"),
|
||||||
|
allow_create: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user