Compare commits

...

42 Commits
demo1 ... main

Author SHA1 Message Date
Jens Timmerman af28efa2d4 added user system 2021-07-04 21:05:00 +02:00
jens 504ab9729d Update 'im/settings.py.prod' 2021-04-11 22:06:31 +00:00
jens 88d1ba7fd3 Update 'im/settings.py.prod' 2021-04-11 21:55:48 +00:00
jens 87880e6915 Update 'im/settings.py.prod' 2021-04-11 21:54:01 +00:00
jens f8a43996ad Merge pull request 'Update 'requirements.txt'' (#19) from jens-patch-2 into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: #19
2021-02-19 10:55:02 +00:00
jens 0c9a04b56e Update 'requirements.txt'
continuous-integration/drone/push Build is passing Details
2021-02-19 10:40:13 +00:00
jens 38c6e8727c Update '.pylintrc'
continuous-integration/drone/push Build is passing Details
2021-02-10 10:22:47 +00:00
jens ebe68fc852 Merge pull request 'fix typos in readme' (#18) from Fre/im:typos into main
continuous-integration/drone/push Build is failing Details
Reviewed-on: #18
2021-02-10 10:19:17 +00:00
Fre Timmerman e74a331258 fix typos 2021-02-09 23:06:27 +01:00
jens 87b7551588 Merge pull request 'add default categories and units' (#17) from fixes into main
continuous-integration/drone/push Build is failing Details
Reviewed-on: #17
2020-11-08 17:02:33 +00:00
jens 2abe451b61 Merge branch 'main' into fixes
continuous-integration/drone/push Build is passing Details
2020-11-08 17:02:14 +00:00
Jens Timmerman 395c8f941e add default categories and units
continuous-integration/drone/push Build is passing Details
2020-11-08 18:00:53 +01:00
jens 921149d02a Merge pull request 'fixes' (#16) from fixes into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: #16
2020-11-08 16:16:32 +00:00
Jens Timmerman d95b8059fd remove fixed todo's
continuous-integration/drone/push Build is passing Details
2020-11-08 17:11:40 +01:00
Jens Timmerman 9a48664042 fix flake8
continuous-integration/drone/push Build is failing Details
2020-11-08 17:10:39 +01:00
Jens Timmerman f46626715e added consume view
continuous-integration/drone/push Build is failing Details
2020-11-08 17:09:37 +01:00
Jens Timmerman bac1492b99 added intial migrations 2020-11-08 16:17:37 +01:00
jens 31662a5273 Merge pull request 'added tests' (#15) from tests into main
continuous-integration/drone/push Build is failing Details
Reviewed-on: #15
2020-11-08 14:48:47 +00:00
jens b997e970b0 Merge branch 'main' into tests
continuous-integration/drone/push Build is failing Details
2020-11-08 14:47:52 +00:00
Jens Timmerman 2976e4d82a enable template debugging
continuous-integration/drone/push Build is failing Details
2020-11-08 15:45:34 +01:00
jens ab4f7cd15b Merge pull request 'Update 'README.md'' (#13) from jens-patch-1 into main
continuous-integration/drone/push Build is passing Details
Reviewed-on: #13
2020-11-08 14:41:02 +00:00
Jens Timmerman b42bea0843 fix test
continuous-integration/drone/push Build is failing Details
2020-11-08 15:40:34 +01:00
Jens Timmerman 3af9a549b3 don't care about unnecessary-pass, do care about invalid names
continuous-integration/drone/push Build is failing Details
2020-11-08 15:35:15 +01:00
Jens Timmerman c4ae189152 fixed flake8
continuous-integration/drone/push Build is failing Details
2020-11-08 15:31:33 +01:00
Jens Timmerman 8622048207 removed unneeded linttest app
continuous-integration/drone/push Build is failing Details
continuous-integration/drone/pr Build is failing Details
2020-11-08 15:24:09 +01:00
Jens Timmerman 21a6eb656c added tests
continuous-integration/drone/pr Build is failing Details
2020-11-08 15:20:52 +01:00
jens f6ebe47eca Update 'README.md'
continuous-integration/drone/push Build is passing Details
2020-10-25 19:38:01 +00:00
jens 90b9fcdde0 Update '.drone.yml'
continuous-integration/drone/push Build is passing Details
2020-10-25 15:07:32 +00:00
jens 5ea03be2e5 Update '.drone.yml'
continuous-integration/drone/push Build is failing Details
2020-10-25 15:04:19 +00:00
jens 21ca4d4484 Update 'README.md'
continuous-integration/drone/push Build is failing Details
2020-10-25 15:03:30 +00:00
jens 4ec1698d9a Update '.drone.yml'
continuous-integration/drone/push Build is failing Details
2020-10-25 15:02:29 +00:00
Jens Timmerman 1f5262f141 Merge branch 'main' of ssh://gitea.caret.be:2223/jens/im into main
continuous-integration/drone/push Build is failing Details
2020-10-25 13:24:28 +01:00
Jens Timmerman 6460918d11 added .drone.yml 2020-10-25 13:23:45 +01:00
jens e0ef6ee58d Update 'inventory/templates/inventory/base.html' 2020-10-07 09:57:04 +00:00
jens e3e858780f Update 'README.md' 2020-09-11 15:33:07 +00:00
jens ec1b972f8b Update 'README.md' 2020-09-11 15:15:01 +00:00
Jens Timmerman e5d1187e25 Merge branch 'main' of ssh://gitea.caret.be:2223/jens/im into main 2020-09-11 02:25:13 +02:00
Jens Timmerman 16dae0e329 remove migrations, we start from 0 here 2020-09-11 02:24:52 +02:00
jens b0e4f86f09 Update 'manage.py' 2020-09-11 00:15:54 +00:00
Jens Timmerman 3c6ea2de37 Merge branch 'main' of ssh://gitea.caret.be:2223/jens/im into main 2020-09-11 00:34:21 +02:00
Jens Timmerman 5c39e6b905
Create ossar-analysis.yml 2020-09-09 00:32:19 +02:00
Jens Timmerman f45a1d98fd
Create codeql-analysis.yml 2020-09-09 00:31:45 +02:00
60 changed files with 718 additions and 493 deletions

23
.drone.yml Normal file
View File

@ -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

47
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -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

49
.github/workflows/ossar-analysis.yml vendored Normal file
View File

@ -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 }}

3
.gitignore vendored
View File

@ -102,3 +102,6 @@ venv.bak/
# mypy
.mypy_cache/
.DS_Store

15
.pylintrc Normal file
View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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}}',
}
}

View File

@ -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')),
]

1
initialdata.json Normal file
View File

@ -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}}]

View File

@ -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)

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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',
),
]

View File

@ -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),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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]])

View File

@ -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>

View File

@ -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">

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -0,0 +1,6 @@
{% extends 'inventory/base.html' %}
{% block content %}
<h1>Item Details</h1>
{{ object }}
{% endblock %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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)

View File

@ -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'),
]

View File

@ -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

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
import os
import sys

View File

@ -1,3 +1,3 @@
#requirements.txt
django>=2.2.8
django>=2.2.19
gunicorn

9
setup.cfg Normal file
View File

@ -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
users/__init__.py Normal file
View File

5
users/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'

5
users/forms.py Normal file
View File

@ -0,0 +1,5 @@
from django.contrib.auth.forms import UserCreationForm
class CustomUserCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
fields = UserCreationForm.Meta.fields + ("email",)

View File

@ -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 %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% block content %}
<h2>Password changed</h2>
<a href="{% url 'im:index' %}">Back to index</a>
{% endblock %}

View File

@ -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 %}

View File

@ -0,0 +1,8 @@
{% extends 'base.html' %}
{% block content %}
<h2>Password reset complete</h2>
<a href="{% url 'login' %}">Back to login</a>
{% endblock %}

View File

@ -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 %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% block content %}
<h2>Password reset done</h2>
<a href="{% url 'login' %}">Back to login</a>
{% endblock %}

View File

@ -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 %}

View File

@ -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 %}

37
users/views.py Normal file
View File

@ -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"))