Compare commits
42 Commits
Author | SHA1 | Date |
---|---|---|
Jens Timmerman | af28efa2d4 | |
jens | 504ab9729d | |
jens | 88d1ba7fd3 | |
jens | 87880e6915 | |
jens | f8a43996ad | |
jens | 0c9a04b56e | |
jens | 38c6e8727c | |
jens | ebe68fc852 | |
Fre Timmerman | e74a331258 | |
jens | 87b7551588 | |
jens | 2abe451b61 | |
Jens Timmerman | 395c8f941e | |
jens | 921149d02a | |
Jens Timmerman | d95b8059fd | |
Jens Timmerman | 9a48664042 | |
Jens Timmerman | f46626715e | |
Jens Timmerman | bac1492b99 | |
jens | 31662a5273 | |
jens | b997e970b0 | |
Jens Timmerman | 2976e4d82a | |
jens | ab4f7cd15b | |
Jens Timmerman | b42bea0843 | |
Jens Timmerman | 3af9a549b3 | |
Jens Timmerman | c4ae189152 | |
Jens Timmerman | 8622048207 | |
Jens Timmerman | 21a6eb656c | |
jens | f6ebe47eca | |
jens | 90b9fcdde0 | |
jens | 5ea03be2e5 | |
jens | 21ca4d4484 | |
jens | 4ec1698d9a | |
Jens Timmerman | 1f5262f141 | |
Jens Timmerman | 6460918d11 | |
jens | e0ef6ee58d | |
jens | e3e858780f | |
jens | ec1b972f8b | |
Jens Timmerman | e5d1187e25 | |
Jens Timmerman | 16dae0e329 | |
jens | b0e4f86f09 | |
Jens Timmerman | 3c6ea2de37 | |
Jens Timmerman | 5c39e6b905 | |
Jens Timmerman | f45a1d98fd |
|
@ -0,0 +1,23 @@
|
|||
kind: pipeline
|
||||
type: exec
|
||||
name: default
|
||||
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: install deps
|
||||
commands:
|
||||
- dnf install -y python3 python3-pip
|
||||
- pip3 install -U Django coverage flake8 pylint django-coverage-plugin pylint-django
|
||||
- name: run unittests
|
||||
commands:
|
||||
- coverage run --source='.' manage.py test --noinput --parallel
|
||||
- name: run flake8
|
||||
commands:
|
||||
- flake8
|
||||
- name: run pylint
|
||||
commands:
|
||||
- pylint --rcfile=.pylintrc -- **/*.py
|
|
@ -0,0 +1,47 @@
|
|||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '0 9 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Override automatic language detection by changing the below list
|
||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||
language: ['python']
|
||||
# Learn more...
|
||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
|
@ -0,0 +1,49 @@
|
|||
# This workflow integrates a collection of open source static analysis tools
|
||||
# with GitHub code scanning. For documentation, or to provide feedback, visit
|
||||
# https://github.com/github/ossar-action
|
||||
name: OSSAR
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
OSSAR-Scan:
|
||||
# OSSAR runs on windows-latest.
|
||||
# ubuntu-latest and macos-latest support coming soon
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
# Checkout your code repository to scan
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Ensure a compatible version of dotnet is installed.
|
||||
# The [Microsoft Security Code Analysis CLI](https://aka.ms/mscadocs) is built with dotnet v3.1.201.
|
||||
# A version greater than or equal to v3.1.201 of dotnet must be installed on the agent in order to run this action.
|
||||
# Remote agents already have a compatible version of dotnet installed and this step may be skipped.
|
||||
# For local agents, ensure dotnet version 3.1.201 or later is installed by including this action:
|
||||
# - name: Install .NET
|
||||
# uses: actions/setup-dotnet@v1
|
||||
# with:
|
||||
# dotnet-version: '3.1.x'
|
||||
|
||||
# Run open source static analysis tools
|
||||
- name: Run OSSAR
|
||||
uses: github/ossar-action@v1
|
||||
id: ossar
|
||||
|
||||
# Upload results to the Security tab
|
||||
- name: Upload OSSAR results
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
with:
|
||||
sarif_file: ${{ steps.ossar.outputs.sarifFile }}
|
|
@ -102,3 +102,6 @@ venv.bak/
|
|||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
|
||||
.DS_Store
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
[MASTER]
|
||||
load-plugins=pylint_django
|
||||
django-settings-module=im.settings
|
||||
|
||||
[FORMAT]
|
||||
max-line-length=120
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=missing-docstring,unnecessary-pass
|
||||
|
||||
[DESIGN]
|
||||
max-parents=13
|
||||
|
||||
[TYPECHECK]
|
||||
generated-members=REQUEST,acl_users,aq_parent,"[a-zA-Z]+_set{1,2}",save,delete
|
23
README.md
23
README.md
|
@ -1,14 +1,18 @@
|
|||
|
||||
[![Build Status](https://drone.caret.be/api/badges/jens/im/status.svg)](https://drone.caret.be/jens/im)
|
||||
|
||||
|
||||
# im
|
||||
Pantry inventory management
|
||||
|
||||
# Inventory management system written in django - python
|
||||
The object of this django app is to keep track of which goods you own, when they will expire, when you should consume them and when you shoud buy more.
|
||||
The object of this django app is to keep track of which goods you own, when they will expire, when you should consume them and when you should buy more.
|
||||
|
||||
## features
|
||||
- Enter items in your pantry (or other locations, with their location) with their expiry date
|
||||
- Items can have a minimum quantity you always want to keep in the pantry
|
||||
- a shopping list is automatically populated with items whose quantity is below this minimum quantity
|
||||
- you can add one off items to this shopping list that are not tracked in the inventory.
|
||||
- you can add one-off items to this shopping list that are not tracked in the inventory.
|
||||
- You get an overview of items per expiry date so you know what to consume first (or throw away)
|
||||
|
||||
- Extensive search and filtering and grouping on locations, categories, units, expiry date using the auto generated django admin
|
||||
|
@ -20,7 +24,7 @@ The object of this django app is to keep track of which goods you own, when they
|
|||
```
|
||||
dnf install -y python3-pip git
|
||||
pip3 install django
|
||||
git clone git@github.com:JensTimmerman/im.git
|
||||
git clone https://gitea.caret.be/jens/im.git
|
||||
cd im
|
||||
|
||||
python3 manage.py migrate
|
||||
|
@ -40,6 +44,7 @@ Now you are running the development server, Visit http://127.0.0.1:8000/admin/
|
|||
|
||||
|
||||
# installation
|
||||
If you use ansible: see https://gitea.caret.be/jens/ansible-role-im
|
||||
```
|
||||
dnf install python3-pip git
|
||||
pip3 install django psycopg2 gunicorn
|
||||
|
@ -68,10 +73,6 @@ DATABASES = {
|
|||
|
||||
clear migrations and start from empty db
|
||||
```
|
||||
rm inventory/migrations/* -rf
|
||||
python3 manage.py migrate
|
||||
|
||||
|
||||
python3 manage.py createsuperuser
|
||||
python3 ./manage.py collectstatic
|
||||
```
|
||||
|
@ -156,7 +157,7 @@ browse to https://im.yourdomain
|
|||
|
||||
# feature requests
|
||||
## High
|
||||
- Add itmes per location, click a location and then start adding items with the location list prepopulated
|
||||
- Add items per location, click a location and then start adding items with the location list prepopulated
|
||||
- auto refresh dropdown boxes after items have been added in admin view, so page reload is not needed
|
||||
- auto parse dates in different date formats when given in a datefield. e.g. 20 12 2020 or 2020 12 20 to 2020-12-20
|
||||
- also make ipad show the numbered keyboard here
|
||||
|
@ -198,9 +199,9 @@ browse to https://im.yourdomain
|
|||
- easily deploy somewhere
|
||||
|
||||
## low
|
||||
- add recepies
|
||||
- shopping list created based on recepy ingredients
|
||||
- auto proposal of recepy based on next expiry dates
|
||||
- add recipes
|
||||
- shopping list created based on recipe ingredients
|
||||
- auto proposal of recipe based on next expiry dates
|
||||
- offer to buy things that have been on shopping list for a while online (in bulk/aggregated)
|
||||
- 3d view of where things are in space
|
||||
- vr to help locate things
|
||||
|
|
|
@ -25,14 +25,19 @@ SECRET_KEY = '-h%7n38#ij*7$pzkv=8-+9axa6o6fk9e4z3x676774f&06-di9'
|
|||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
ALLOWED_HOSTS = [
|
||||
'0.0.0.0',
|
||||
]
|
||||
|
||||
LOGIN_REDIRECT_URL = "im:index"
|
||||
LOGOUT_REDIRECT_URL = "im:index"
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'users.apps.UsersConfig',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
|
@ -64,6 +69,7 @@ TEMPLATES = [
|
|||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
'debug': DEBUG,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
@ -100,6 +106,10 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
},
|
||||
]
|
||||
|
||||
# enabling this may speed up testing
|
||||
# PASSWORD_HASHERS = [
|
||||
# 'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||
# ]
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.1/topics/i18n/
|
||||
|
@ -119,3 +129,7 @@ USE_TZ = True
|
|||
# https://docs.djangoproject.com/en/2.1/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
|
||||
EMAIL_HOST = 'mail.caret.be'
|
||||
EMAIL_PORT = '25'
|
||||
|
|
|
@ -26,10 +26,14 @@ SECRET_KEY = '{{im_secret_key}}'
|
|||
DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = [
|
||||
{{im_domain}}
|
||||
'{{im_domain}}'
|
||||
]
|
||||
|
||||
|
||||
ADMINS = [('admin', '{{im_admin_email}}')]
|
||||
|
||||
EMAIL_HOST = '{{im_mail_server}}'
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
|
@ -82,7 +86,7 @@ DATABASES = {
|
|||
'NAME': '{{im_db_name}}',
|
||||
'USER': '{{im_db_user}}',
|
||||
'PASSWORD': '{{im_db_password}}',
|
||||
'HOST': '{{im_db_server_ip}}',
|
||||
'HOST': '{{im_db_server}}',
|
||||
'PORT': '{{im_db_port}}',
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,11 @@ Including another URLconf
|
|||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
from users.views import register
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('accounts/', include('django.contrib.auth.urls')),
|
||||
path("register/", register, name="register"),
|
||||
path('', include('inventory.urls')),
|
||||
]
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
[{"model": "inventory.category", "pk": 1, "fields": {"name": "UNCATEGORIZED"}}, {"model": "inventory.category", "pk": 2, "fields": {"name": "GENRAL_STOCK"}}, {"model": "inventory.category", "pk": 3, "fields": {"name": "CANS"}}, {"model": "inventory.category", "pk": 4, "fields": {"name": "BREAKFAST"}}, {"model": "inventory.category", "pk": 5, "fields": {"name": "SAVORIES"}}, {"model": "inventory.category", "pk": 6, "fields": {"name": "SWEETS"}}, {"model": "inventory.category", "pk": 7, "fields": {"name": "SNACKS"}}, {"model": "inventory.category", "pk": 8, "fields": {"name": "SPICES"}}, {"model": "inventory.category", "pk": 9, "fields": {"name": "DRINKS"}}, {"model": "inventory.unit", "pk": 1, "fields": {"name": "Mililiter"}}, {"model": "inventory.unit", "pk": 2, "fields": {"name": "Grams"}}, {"model": "inventory.unit", "pk": 3, "fields": {"name": "Roll"}}, {"model": "inventory.unit", "pk": 4, "fields": {"name": "Tube"}}, {"model": "inventory.unit", "pk": 5, "fields": {"name": "Bag"}}, {"model": "inventory.unit", "pk": 6, "fields": {"name": "Bar"}}, {"model": "inventory.unit", "pk": 7, "fields": {"name": "Pack"}}, {"model": "inventory.location", "pk": 1, "fields": {"name": "Caret", "in_location": null}}]
|
|
@ -9,7 +9,6 @@ class PantryItemInLine(admin.TabularInline):
|
|||
extra = 1
|
||||
|
||||
|
||||
|
||||
class LocationInLine(admin.TabularInline):
|
||||
model = Location
|
||||
extra = 1
|
||||
|
@ -17,16 +16,20 @@ class LocationInLine(admin.TabularInline):
|
|||
|
||||
def upper_case_name(obj):
|
||||
return obj.name.upper()
|
||||
|
||||
|
||||
upper_case_name.short_description = 'Name'
|
||||
|
||||
|
||||
def capitalize_name(obj):
|
||||
return obj.name.capitalize()
|
||||
|
||||
|
||||
upper_case_name.short_description = 'Name'
|
||||
|
||||
|
||||
class PantryItemLineAdmin(admin.ModelAdmin):
|
||||
list_filter = ['expiry_date', 'pantry_item__unit', 'pantry_item', 'pantry_item__min_quantity']
|
||||
list_filter = ['expiry_date', 'pantry_item__unit', 'pantry_item', 'pantry_item__min_quantity', 'location']
|
||||
search_fields = ['info', 'pantry_item__name', 'pantry_item__info']
|
||||
autocomplete_fields = ['pantry_item']
|
||||
|
||||
|
@ -59,13 +62,12 @@ class LocationAdmin(AutocompleteAdmin):
|
|||
|
||||
|
||||
class PantryItemAdmin(admin.ModelAdmin):
|
||||
list_filter = ['category', 'unit', 'min_quantity', 'location']
|
||||
list_filter = ['category', 'unit', 'min_quantity']
|
||||
search_fields = ['info', 'name', 'category__name', 'unit__name']
|
||||
autocomplete_fields = ['category', 'unit']
|
||||
inlines = [PantryItemInLine]
|
||||
|
||||
# TODO: make category a model
|
||||
#autocomplete_fields = ['category',]
|
||||
autocomplete_fields = ['category', ]
|
||||
fields = (
|
||||
'name',
|
||||
'category',
|
||||
|
@ -93,6 +95,7 @@ class ShoppingListItemAdmin(PantryItemAdmin):
|
|||
'info',
|
||||
)
|
||||
|
||||
|
||||
admin.site.register(PantryItem, PantryItemAdmin)
|
||||
admin.site.register(ShoppingListItem, ShoppingListItemAdmin)
|
||||
admin.site.register(PantryItemLine, PantryItemLineAdmin)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-16 10:49
|
||||
# Generated by Django 3.1.3 on 2020-11-08 15:14
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
@ -12,20 +12,61 @@ class Migration(migrations.Migration):
|
|||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Location',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('in_location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.location')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PantryItem',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('min_quantity', models.IntegerField(default=1)),
|
||||
('expiry_duration', models.IntegerField(blank=True, null=True)),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('info', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.category')),
|
||||
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.location')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Unit',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ShoppingListItem',
|
||||
fields=[
|
||||
('pantryitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='inventory.pantryitem')),
|
||||
],
|
||||
bases=('inventory.pantryitem',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PantryItemLine',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('quantity', models.IntegerField(default=1)),
|
||||
('expiry_date', models.DateTimeField()),
|
||||
('pantry_item', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='inventory.PantryItem')),
|
||||
('expiry_date', models.DateField(blank=True, null=True)),
|
||||
('size', models.IntegerField(default=1)),
|
||||
('info', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('pantry_item', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='inventory.pantryitem')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pantryitem',
|
||||
name='unit',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.unit'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-16 11:10
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='pantryitem',
|
||||
name='category',
|
||||
field=models.CharField(choices=[('UC', 'UNCATEGORIZED'), ('GS', 'GENRAL_STOCK'), ('CANS', 'CANS'), ('BR', 'BREAKFAST'), ('SAV', 'SAVORIES'), ('SW', 'SWEETS'), ('SN', 'SNACKS'), ('SP', 'SPICES')], default='UN', max_length=200),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitemline',
|
||||
name='expiry_date',
|
||||
field=models.DateField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitemline',
|
||||
name='pantry_item',
|
||||
field=models.ForeignKey(default='UN', on_delete=django.db.models.deletion.PROTECT, to='inventory.PantryItem'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 3.2.4 on 2021-07-04 18:10
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='pantryitem',
|
||||
name='location',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pantryitemline',
|
||||
name='location',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.location'),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-16 11:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0002_auto_20180916_1110'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='pantryitem',
|
||||
name='min_quantity',
|
||||
field=models.IntegerField(default=1),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-16 11:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0003_pantryitem_min_quantity'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='pantryitemline',
|
||||
name='size',
|
||||
field=models.CharField(max_length=20, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitem',
|
||||
name='category',
|
||||
field=models.CharField(choices=[('UC', 'UNCATEGORIZED'), ('GS', 'GENRAL_STOCK'), ('CANS', 'CANS'), ('BR', 'BREAKFAST'), ('SAV', 'SAVORIES'), ('SW', 'SWEETS'), ('SN', 'SNACKS'), ('SP', 'SPICES'), ('DR', 'DRINKS')], max_length=200),
|
||||
),
|
||||
]
|
|
@ -1,28 +0,0 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-16 11:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0004_auto_20180916_1127'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='pantryitem',
|
||||
name='info',
|
||||
field=models.CharField(max_length=200, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pantryitemline',
|
||||
name='info',
|
||||
field=models.CharField(max_length=200, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitemline',
|
||||
name='size',
|
||||
field=models.IntegerField(default=1),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-16 11:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0005_auto_20180916_1154'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='pantryitem',
|
||||
name='unit',
|
||||
field=models.CharField(choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags')], max_length=20, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pantryitemline',
|
||||
name='unit',
|
||||
field=models.CharField(choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags')], max_length=20, null=True),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-16 12:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0006_auto_20180916_1156'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='pantryitem',
|
||||
name='info',
|
||||
field=models.CharField(blank=True, max_length=200, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitemline',
|
||||
name='info',
|
||||
field=models.CharField(blank=True, max_length=200, null=True),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-16 12:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0007_auto_20180916_1203'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='pantryitem',
|
||||
name='unit',
|
||||
field=models.CharField(blank=True, choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags')], max_length=20, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitemline',
|
||||
name='unit',
|
||||
field=models.CharField(blank=True, choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags')], max_length=20, null=True),
|
||||
),
|
||||
]
|
|
@ -1,28 +0,0 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-16 12:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0008_auto_20180916_1205'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='pantryitem',
|
||||
name='unit',
|
||||
field=models.CharField(blank=True, choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags'), ('Roll', 'Roll')], max_length=20, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitemline',
|
||||
name='expiry_date',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitemline',
|
||||
name='unit',
|
||||
field=models.CharField(blank=True, choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags'), ('Roll', 'Roll')], max_length=20, null=True),
|
||||
),
|
||||
]
|
|
@ -1,33 +0,0 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-16 12:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0009_auto_20180916_1219'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='pantryitem',
|
||||
name='min_quantity',
|
||||
field=models.DecimalField(decimal_places=3, default=1, max_digits=32),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitem',
|
||||
name='unit',
|
||||
field=models.CharField(blank=True, choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags'), ('Roll', 'Roll'), ('Tube', 'Tube'), ('Bag', 'Bag')], max_length=20, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitemline',
|
||||
name='size',
|
||||
field=models.DecimalField(decimal_places=3, default=1, max_digits=32),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitemline',
|
||||
name='unit',
|
||||
field=models.CharField(blank=True, choices=[('L', 'Liter'), ('GR', 'Grams'), ('Bags', 'Bags'), ('Roll', 'Roll'), ('Tube', 'Tube'), ('Bag', 'Bag')], max_length=20, null=True),
|
||||
),
|
||||
]
|
|
@ -1,57 +0,0 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-23 20:25
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0010_auto_20180916_1252'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Unit',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('unit', models.CharField(max_length=50)),
|
||||
],
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='pantryitemline',
|
||||
name='unit',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitem',
|
||||
name='category',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='inventory.Category'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitem',
|
||||
name='min_quantity',
|
||||
field=models.IntegerField(default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitem',
|
||||
name='unit',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.Unit'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitemline',
|
||||
name='pantry_item',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='inventory.PantryItem'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitemline',
|
||||
name='size',
|
||||
field=models.IntegerField(default=1),
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-23 20:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0011_auto_20180923_2025'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='unit',
|
||||
old_name='unit',
|
||||
new_name='name',
|
||||
),
|
||||
]
|
|
@ -1,18 +0,0 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-27 17:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0012_auto_20180923_2037'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='pantryitem',
|
||||
name='expiry_duration',
|
||||
field=models.DurationField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -1,32 +0,0 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-30 14:06
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0013_pantryitem_expiry_duration'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Location',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('in_location', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='inventory.Location')),
|
||||
],
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitem',
|
||||
name='expiry_duration',
|
||||
field=models.IntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pantryitem',
|
||||
name='location',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.Location'),
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-30 14:08
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0014_auto_20180930_1406'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='in_location',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.Location'),
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 2.1.1 on 2018-09-30 14:09
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0015_auto_20180930_1408'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='location',
|
||||
name='in_location',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.Location'),
|
||||
),
|
||||
]
|
|
@ -1,26 +0,0 @@
|
|||
# Generated by Django 2.1.2 on 2018-10-07 11:08
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0016_auto_20180930_1409'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ShoppingListItem',
|
||||
fields=[
|
||||
('pantryitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='inventory.PantryItem')),
|
||||
],
|
||||
bases=('inventory.pantryitem',),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='pantryitem',
|
||||
name='location',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.Location'),
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 2.1.2 on 2018-10-09 10:32
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('inventory', '0017_auto_20181007_1108'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='pantryitem',
|
||||
name='category',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='inventory.Category'),
|
||||
),
|
||||
]
|
|
@ -39,7 +39,7 @@ class Unit(models.Model):
|
|||
|
||||
class Location(models.Model):
|
||||
"""Location for a pantry item line"""
|
||||
name = models.CharField(max_length=200, null=True, blank=True)
|
||||
name = models.CharField(max_length=200)
|
||||
in_location = models.ForeignKey("self", on_delete=models.PROTECT, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -47,6 +47,9 @@ class Location(models.Model):
|
|||
return self.name + ' ' + str(self.in_location)
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
unique_together = ('name', 'in_location')
|
||||
|
||||
|
||||
class PantryItem(models.Model):
|
||||
"""A think you keep in your pantry """
|
||||
|
@ -57,12 +60,9 @@ class PantryItem(models.Model):
|
|||
# if expiry duration is set the expiration date for a pantryitemline will be set to now + duration on save
|
||||
# represents days
|
||||
expiry_duration = models.IntegerField(null=True, blank=True)
|
||||
name = models.CharField(max_length=200)
|
||||
name = models.CharField(max_length=200)
|
||||
unit = models.ForeignKey(Unit, on_delete=models.PROTECT, null=True)
|
||||
info = models.CharField(max_length=200, null=True, blank=True)
|
||||
# location is saved on a per item base, not itemline
|
||||
# you can have multiple pantries with subpantries
|
||||
location = models.ForeignKey(Location, on_delete=models.PROTECT, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
@ -78,15 +78,17 @@ class PantryItemLine(models.Model):
|
|||
pantry_item = models.ForeignKey(PantryItem, on_delete=models.PROTECT)
|
||||
quantity = models.IntegerField(default=1)
|
||||
expiry_date = models.DateField(null=True, blank=True)
|
||||
size = models.IntegerField(default=1) #, decimal_places=3, max_digits=32)
|
||||
size = models.IntegerField(default=1)
|
||||
info = models.CharField(max_length=200, null=True, blank=True)
|
||||
|
||||
location = models.ForeignKey(Location, on_delete=models.PROTECT, null=True, blank=True)
|
||||
|
||||
def unit(self):
|
||||
return self.pantry_item.unit
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('pantryitemlinedetail', kwargs={'pk': self.pk})
|
||||
return reverse('im:pantryitemlinedetail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return ' '.join([str(x) for x in [self.pantry_item.name, self.quantity, 'X', self.size, self.pantry_item.unit]])
|
||||
|
||||
return ' '.join([str(x) for x in [self.pantry_item.name, self.quantity, 'X',
|
||||
self.size, self.pantry_item.unit]])
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||
<title>Inventory Management</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="/">Inventory Management</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'im:consumelist' %} ">Consume</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'im:additemline' %} ">Add</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'im:shoppinglist' %} ">Shopping List</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'im:expirations' %} ">Expirations</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
{% if user.is_authenticated %}
|
||||
<a class="nav-link" href="{% url 'logout' %}">Logout {{ user.username }}</a>
|
||||
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'password_change' %} ">Change Password</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'admin:index' %} ">Admin</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<a class="nav-link" href="{% url 'login' %}">Login</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
|
||||
<!--li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Dropdown
|
||||
</a>
|
||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||
<a class="dropdown-item" href="#">Action</a>
|
||||
<a class="dropdown-item" href="#">Another action</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="#">Something else here</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled" href="#">Disabled</a>
|
||||
</li!-->
|
||||
</ul>
|
||||
<form class="form-inline my-2 my-lg-0" action="{% url 'admin:inventory_pantryitem_changelist' %}" method="GET">
|
||||
<input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search" name="q">
|
||||
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-8">
|
||||
{% block content %}
|
||||
BASE TEMPLATE
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
|
||||
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
|
||||
crossorigin="anonymous"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -4,13 +4,13 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
|
||||
<title>Pantry Inventory</title>
|
||||
<title>Inventory Management</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<a class="navbar-brand" href="/">Pantry inventory</a>
|
||||
<a class="navbar-brand" href="/">Inventory Management</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
@ -18,10 +18,10 @@
|
|||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'im:consume' %} ">Consume</a>
|
||||
<a class="nav-link" href="{% url 'im:consumelist' %} ">Consume</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'admin:inventory_pantryitem_changelist' %} ">Add</a>
|
||||
<a class="nav-link" href="{% url 'im:additemline' %} ">Add</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
|
@ -31,8 +31,22 @@
|
|||
<a class="nav-link" href="{% url 'im:expirations' %} ">Expirations</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
{% if user.is_authenticated %}
|
||||
<a class="nav-link" href="{% url 'logout' %}">Logout {{ user.username }}</a>
|
||||
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'password_change' %} ">Change Password</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'admin:index' %} ">Admin</a>
|
||||
</li>
|
||||
</li>
|
||||
{% else %}
|
||||
<a class="nav-link" href="{% url 'login' %}">Login</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
|
||||
<!--li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{% extends 'inventory/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h2>Add Category</h2>
|
||||
<form action="." method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -1,5 +1,10 @@
|
|||
{% extends 'inventory/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
TODO: add form to mark items as consumed
|
||||
<h1>Consume</h1>
|
||||
Change quantity for "{{ object }}"
|
||||
<form method="post">{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Update">
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
{% extends 'inventory/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Consume</h1>
|
||||
{% if pis %}
|
||||
{% regroup pis by expiry_date as pis_by_date %}
|
||||
{% for date in pis_by_date %}
|
||||
<div class="content">
|
||||
<h2>{{ date.grouper|date:"d F Y" }}</h2>
|
||||
<ul>
|
||||
{% for pi in date.list %}
|
||||
<li><a href="{% url 'im:consume' pi.id %}">{{ pi|title}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>No Pantry Items are available.</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -17,5 +17,16 @@ Expirations: See items that will expire soon
|
|||
</p>
|
||||
<p>
|
||||
TODO: add items to your shopping list below
|
||||
</p>
|
||||
<p>
|
||||
|
||||
TODO: see expirted items below?
|
||||
</p>
|
||||
<p>
|
||||
TODO: when no categories are found, show getting started, otherwise nit.
|
||||
|
||||
You will probably want to add some default Categories and Units
|
||||
|
||||
run TODO: https://pypi.org/project/django-smuggler/
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{% extends 'inventory/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h2>Add Location</h2>
|
||||
<form action="." method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,13 @@
|
|||
{% extends 'inventory/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
If you can not find the Category you want to add a new Line item from, add a new one <a href="../addcategory/">here</a>
|
||||
|
||||
<h2>Add Item Line</h2>
|
||||
<form action="/additemline/" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,6 @@
|
|||
{% extends 'inventory/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Item Details</h1>
|
||||
{{ object }}
|
||||
{% endblock %}
|
|
@ -0,0 +1,17 @@
|
|||
{% extends 'inventory/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<p>
|
||||
If you can not find the Pantry Item you want to add a new Line item from, add a new one <a href="../additem/">here</a>
|
||||
</p>
|
||||
<p>
|
||||
If you can not find the Location you want to add a new Line item from, add a new one <a href="../addlocation/">here</a>
|
||||
</p>
|
||||
<h2>Add Item Line</h2>
|
||||
<form action="/additemline/" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,10 @@
|
|||
{% extends 'inventory/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Units</h2>
|
||||
<ul>
|
||||
{% for unit in object_list %}
|
||||
<li>{{ unit.name }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
|
@ -1,3 +1,25 @@
|
|||
from django.test import TestCase
|
||||
from django.test import Client
|
||||
|
||||
# Create your tests here.
|
||||
from inventory.models import PantryItem, Category
|
||||
|
||||
|
||||
class PantryItemTestCase(TestCase):
|
||||
""" simple test case for a model"""
|
||||
def setUp(self):
|
||||
cat = Category.objects.create(name="UNCATEGORIZED")
|
||||
PantryItem.objects.create(name="testitem", category=cat)
|
||||
|
||||
def test_pantryitem_looksok(self):
|
||||
"""Pantryitems to string is ok"""
|
||||
testitem = PantryItem.objects.get(name="testitem")
|
||||
self.assertEqual(str(testitem), 'testitem')
|
||||
|
||||
|
||||
class InterFaceTestCase(TestCase):
|
||||
"""Simple test case for the web interface"""
|
||||
|
||||
def test_consume_view_exists(self):
|
||||
client = Client()
|
||||
response = client.get('/consume/')
|
||||
self.assertTrue(b"TODO" not in response.content)
|
||||
|
|
|
@ -4,10 +4,14 @@ from . import views
|
|||
app_name = 'im'
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('consume/', views.consume, name='consume'),
|
||||
path('items/<pk>', views.PantryItemLineView.as_view(), name='pantryitemlinedetail'),
|
||||
path('consume/', views.ConsumeList.as_view(), name='consumelist'),
|
||||
path('consume/<pk>/', views.Consume.as_view(), name='consume'),
|
||||
path('shoppinglist/', views.Shoppinglist.as_view(), name='shoppinglist'),
|
||||
# TODO: add exiperes before X date?
|
||||
# TODO: add categories
|
||||
# TODO: add pantry item selection
|
||||
path('expirations/', views.Expirations.as_view(), name='expirations'),
|
||||
path('units/', views.UnitView.as_view(), name='units'),
|
||||
path('additemline/', views.PantryItemLineCreateView.as_view(), name='additemline'),
|
||||
path('additem/', views.PantryItemCreateView.as_view(), name='additem'),
|
||||
path('addcategory/', views.CategoryCreateView.as_view(), name='addcategory'),
|
||||
path('addlocation/', views.LocationCreateView.as_view(), name='addlocation'),
|
||||
]
|
||||
|
|
|
@ -1,11 +1,45 @@
|
|||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.views import generic
|
||||
from django.views.generic.edit import CreateView, DeleteView, UpdateView
|
||||
from django.db.models import F, Sum, Q
|
||||
|
||||
|
||||
from .models import PantryItem, PantryItemLine
|
||||
from django.views.generic import ListView
|
||||
from inventory.models import Unit, Location
|
||||
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic.edit import CreateView
|
||||
|
||||
from .models import PantryItem, PantryItemLine, Category
|
||||
|
||||
class PantryItemLineCreateView(LoginRequiredMixin, CreateView):
|
||||
model = PantryItemLine
|
||||
fields = '__all__'
|
||||
success_url = '.'
|
||||
|
||||
|
||||
class PantryItemCreateView(LoginRequiredMixin, CreateView):
|
||||
model = PantryItem
|
||||
fields = '__all__'
|
||||
success_url = '.'
|
||||
|
||||
|
||||
class CategoryCreateView(LoginRequiredMixin, CreateView):
|
||||
model = Category
|
||||
fields = '__all__'
|
||||
success_url = '.'
|
||||
|
||||
class LocationCreateView(LoginRequiredMixin, CreateView):
|
||||
model = Location
|
||||
fields = '__all__'
|
||||
success_url = '.'
|
||||
|
||||
|
||||
|
||||
|
||||
class UnitView(ListView):
|
||||
model = Unit
|
||||
|
||||
|
||||
# Create your views here.
|
||||
|
||||
|
@ -14,10 +48,6 @@ def index(request):
|
|||
return render(request, "inventory/index.html")
|
||||
|
||||
|
||||
def consume(request):
|
||||
return render(request, "inventory/consume.html")
|
||||
|
||||
|
||||
class Shoppinglist(generic.ListView):
|
||||
template_name = "inventory/shoppinglist.html"
|
||||
context_object_name = 'pis'
|
||||
|
@ -26,9 +56,10 @@ class Shoppinglist(generic.ListView):
|
|||
"""
|
||||
Return pantryitems for which we have None itemlines or Itemlines whith a total quantitly less than required
|
||||
"""
|
||||
return PantryItem.objects.annotate(total_quantity=Sum(F('pantryitemline__quantity') *
|
||||
F('pantryitemline__size'))).filter(Q(min_quantity__gt=F('total_quantity')) | Q(pantryitemline=None,
|
||||
min_quantity__gt=0))
|
||||
return PantryItem.objects.annotate(
|
||||
total_quantity=Sum(F('pantryitemline__quantity') *
|
||||
F('pantryitemline__size'))
|
||||
).filter(Q(min_quantity__gt=F('total_quantity')) | Q(pantryitemline=None, min_quantity__gt=0))
|
||||
|
||||
|
||||
class Expirations(generic.ListView):
|
||||
|
@ -37,3 +68,25 @@ class Expirations(generic.ListView):
|
|||
|
||||
def get_queryset(self):
|
||||
return PantryItemLine.objects.exclude(expiry_date__isnull=True).exclude(quantity=0).order_by('expiry_date')
|
||||
|
||||
|
||||
class ConsumeList(generic.ListView):
|
||||
template_name = "inventory/consumelist.html"
|
||||
context_object_name = 'pis'
|
||||
|
||||
def get_queryset(self):
|
||||
return PantryItemLine.objects.exclude(quantity=0).order_by('expiry_date')
|
||||
|
||||
|
||||
class Consume(generic.UpdateView):
|
||||
template_name = "inventory/consume.html"
|
||||
fields = ['quantity']
|
||||
|
||||
context_object_name = 'pis'
|
||||
|
||||
def get_queryset(self):
|
||||
return PantryItemLine.objects.exclude(quantity=0).order_by('expiry_date')
|
||||
|
||||
|
||||
class PantryItemLineView(generic.DetailView):
|
||||
model = PantryItemLine
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#requirements.txt
|
||||
django>=2.2.8
|
||||
django>=2.2.19
|
||||
gunicorn
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
[flake8]
|
||||
max-line-length = 120
|
||||
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
|
||||
|
||||
[coverage:run]
|
||||
include = '.'
|
||||
omit = *migrations*, *tests*
|
||||
plugins =
|
||||
django_coverage_plugin
|
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
name = 'users'
|
|
@ -0,0 +1,5 @@
|
|||
from django.contrib.auth.forms import UserCreationForm
|
||||
|
||||
class CustomUserCreationForm(UserCreationForm):
|
||||
class Meta(UserCreationForm.Meta):
|
||||
fields = UserCreationForm.Meta.fields + ("email",)
|
|
@ -0,0 +1,15 @@
|
|||
{% extends 'inventory/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Login</h2>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
|
||||
<a href="{% url 'im:index' %}">Back to index</a>
|
||||
<a href="{% url 'register' %}">Register</a>
|
||||
<a href="{% url 'password_reset' %}">Reset Password</a>
|
||||
{% endblock %}
|
|
@ -0,0 +1,7 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Password changed</h2>
|
||||
|
||||
<a href="{% url 'im:index' %}">Back to index</a>
|
||||
{% endblock %}
|
|
@ -0,0 +1,13 @@
|
|||
{% extends 'inventory/base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Change Password</h2>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Change">
|
||||
</form>
|
||||
|
||||
<a href="{% url 'im:index' %}">Back to index</a>
|
||||
{% endblock %}
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Password reset complete</h2>
|
||||
|
||||
<a href="{% url 'login' %}">Back to login</a>
|
||||
{% endblock %}
|
|
@ -0,0 +1,11 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Confirm password reset</h2>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Confirm">
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,7 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Password reset done</h2>
|
||||
|
||||
<a href="{% url 'login' %}">Back to login</a>
|
||||
{% endblock %}
|
|
@ -0,0 +1,13 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Send password reset link</h2>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Reset">
|
||||
</form>
|
||||
|
||||
<a href="{% url 'im:index' %}">Back to index</a>
|
||||
{% endblock %}
|
|
@ -0,0 +1,13 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Register</h2>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{form}}
|
||||
<input type="submit" value="Register">
|
||||
</form>
|
||||
|
||||
<a href="{% url 'login' %}">Back to login</a>
|
||||
{% endblock %}
|
|
@ -0,0 +1,37 @@
|
|||
from django.contrib.auth import login
|
||||
|
||||
from django.shortcuts import redirect, render
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from users.forms import CustomUserCreationForm
|
||||
|
||||
|
||||
def dashboard(request):
|
||||
|
||||
return render(request, "users/dashboard.html")
|
||||
|
||||
|
||||
def register(request):
|
||||
|
||||
if request.method == "GET":
|
||||
|
||||
return render(
|
||||
|
||||
request, "users/register.html",
|
||||
|
||||
{"form": CustomUserCreationForm}
|
||||
|
||||
)
|
||||
|
||||
elif request.method == "POST":
|
||||
|
||||
form = CustomUserCreationForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
user = form.save()
|
||||
|
||||
login(request, user)
|
||||
|
||||
return redirect(reverse("index"))
|
Loading…
Reference in New Issue