diff --git a/.gitignore b/.gitignore index 8b40fdf3e5..ead4b16409 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ __pycache__/ # Distribution / packaging .Python env/ -build/ +./build/ develop-eggs/ dist/ downloads/ diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py new file mode 100644 index 0000000000..1025f74e34 --- /dev/null +++ b/InvenTree/InvenTree/helpers.py @@ -0,0 +1,18 @@ +import inspect +from enum import Enum + + +class ChoiceEnum(Enum): + """ Helper class to provide enumerated choice values for integer fields + """ + # http://blog.richard.do/index.php/2014/02/how-to-use-enums-for-django-field-choices/ + + @classmethod + def choices(cls): + # get all members of the class + members = inspect.getmembers(cls, lambda m: not(inspect.isroutine(m))) + # filter down to just properties + props = [m for m in members if not(m[0][:2] == '__')] + # format into django choice tuple + choices = tuple([(str(p[1].value), p[0]) for p in props]) + return choices \ No newline at end of file diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index bc458b112d..6ce09ad104 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -50,6 +50,7 @@ INSTALLED_APPS = [ 'part.apps.PartConfig', 'stock.apps.StockConfig', 'supplier.apps.SupplierConfig', + 'build.apps.BuildConfig', ] MIDDLEWARE = [ diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 18eb9784ec..da9e2573a9 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -11,6 +11,8 @@ from stock.urls import stock_urls # from supplier.urls import supplier_api_urls, supplier_api_part_urls from supplier.urls import supplier_urls +from build.urls import build_urls + from django.conf import settings from django.conf.urls.static import static @@ -68,6 +70,7 @@ urlpatterns = [ url(r'^part/', include(part_urls)), url(r'^stock/', include(stock_urls)), url(r'^supplier/', include(supplier_urls)), + url(r'^build/', include(build_urls)), url(r'^admin/', admin.site.urls), url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')), diff --git a/InvenTree/project/__init__.py b/InvenTree/build/__init__.py similarity index 100% rename from InvenTree/project/__init__.py rename to InvenTree/build/__init__.py diff --git a/InvenTree/build/admin.py b/InvenTree/build/admin.py new file mode 100644 index 0000000000..dc9590890e --- /dev/null +++ b/InvenTree/build/admin.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.contrib import admin + +from .models import Build + + +class BuildAdmin(admin.ModelAdmin): + + list_display = ('status', ) + + +admin.site.register(Build, BuildAdmin) diff --git a/InvenTree/project/apps.py b/InvenTree/build/apps.py similarity index 51% rename from InvenTree/project/apps.py rename to InvenTree/build/apps.py index 6b7b79986e..a5b69d365a 100644 --- a/InvenTree/project/apps.py +++ b/InvenTree/build/apps.py @@ -1,7 +1,8 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.apps import AppConfig -class ProjectConfig(AppConfig): - name = 'project' +class BuildConfig(AppConfig): + name = 'build' diff --git a/InvenTree/build/migrations/0001_initial.py b/InvenTree/build/migrations/0001_initial.py new file mode 100644 index 0000000000..2827070df8 --- /dev/null +++ b/InvenTree/build/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-04-16 14:03 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('part', '0019_auto_20180416_1249'), + ] + + operations = [ + migrations.CreateModel( + name='Build', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.PositiveIntegerField(choices=[(b'20', b'Allocated'), (b'30', b'Cancelled'), (b'40', b'Complete'), (b'10', b'Pending')], default=10)), + ], + ), + migrations.CreateModel( + name='BuildOutput', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('part_name', models.CharField(max_length=255)), + ('quantity', models.PositiveIntegerField(default=1, help_text='Number of parts to build', validators=[django.core.validators.MinValueValidator(1)])), + ('build', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='outputs', to='build.Build')), + ('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part')), + ], + ), + migrations.AlterUniqueTogether( + name='buildoutput', + unique_together=set([('part', 'build')]), + ), + ] diff --git a/InvenTree/build/migrations/0002_auto_20180416_1423.py b/InvenTree/build/migrations/0002_auto_20180416_1423.py new file mode 100644 index 0000000000..59e747efbc --- /dev/null +++ b/InvenTree/build/migrations/0002_auto_20180416_1423.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-04-16 14:23 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0001_initial'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='buildoutput', + unique_together=set([]), + ), + migrations.RemoveField( + model_name='buildoutput', + name='build', + ), + migrations.RemoveField( + model_name='buildoutput', + name='part', + ), + migrations.AddField( + model_name='build', + name='quantity', + field=models.PositiveIntegerField(default=1, help_text='Number of parts to build', validators=[django.core.validators.MinValueValidator(1)]), + ), + migrations.DeleteModel( + name='BuildOutput', + ), + ] diff --git a/InvenTree/build/migrations/0003_build_part.py b/InvenTree/build/migrations/0003_build_part.py new file mode 100644 index 0000000000..a832f1da73 --- /dev/null +++ b/InvenTree/build/migrations/0003_build_part.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-04-16 14:28 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0019_auto_20180416_1249'), + ('build', '0002_auto_20180416_1423'), + ] + + operations = [ + migrations.AddField( + model_name='build', + name='part', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='builds', to='part.Part'), + preserve_default=False, + ), + ] diff --git a/InvenTree/project/migrations/__init__.py b/InvenTree/build/migrations/__init__.py similarity index 100% rename from InvenTree/project/migrations/__init__.py rename to InvenTree/build/migrations/__init__.py diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py new file mode 100644 index 0000000000..a55abb15fc --- /dev/null +++ b/InvenTree/build/models.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models +from django.core.validators import MinValueValidator + +from InvenTree.helpers import ChoiceEnum + +from part.models import Part + +class Build(models.Model): + """ A Build object organises the creation of new parts from the component parts + It uses the part BOM to generate new parts. + Parts are then taken from stock + """ + + class BUILD_STATUS(ChoiceEnum): + # The build is 'pending' - no action taken yet + Pending = 10 + + # The parts required for this build have been allocated + Allocated = 20 + + # The build has been cancelled (parts unallocated) + Cancelled = 30 + + # The build is complete! + Complete = 40 + + # Status of the build + status = models.PositiveIntegerField(default=BUILD_STATUS.Pending.value, + choices=BUILD_STATUS.choices()) + + part = models.ForeignKey(Part, on_delete=models.CASCADE, + related_name='builds') + + quantity = models.PositiveIntegerField(default=1, + validators=[MinValueValidator(1)], + help_text='Number of parts to build') diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html new file mode 100644 index 0000000000..1ec5b4a8e3 --- /dev/null +++ b/InvenTree/build/templates/build/detail.html @@ -0,0 +1,3 @@ +{% include "base.html" %} + +Build detail for Build No. {{ build.id }}. \ No newline at end of file diff --git a/InvenTree/build/templates/build/index.html b/InvenTree/build/templates/build/index.html new file mode 100644 index 0000000000..aaf722d8d1 --- /dev/null +++ b/InvenTree/build/templates/build/index.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block content %} + +

Builds

+ +{% endblock %} diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py new file mode 100644 index 0000000000..c2de5b3ab1 --- /dev/null +++ b/InvenTree/build/tests.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +# from django.test import TestCase + +# Create your tests here. diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py new file mode 100644 index 0000000000..b3552ce5e9 --- /dev/null +++ b/InvenTree/build/urls.py @@ -0,0 +1,17 @@ +from django.conf.urls import url, include +from django.views.generic.base import RedirectView + +from . import views + +build_detail_urls = [ + + url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'), +] + +build_urls = [ + # url(r'new/?', views.BuildCreate.as_view(), name='build-create'), + + url(r'^(?P\d+)/', include(build_detail_urls)), + + url(r'.*$', views.BuildIndex.as_view(), name='build-index'), +] \ No newline at end of file diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py new file mode 100644 index 0000000000..ae26c1f99b --- /dev/null +++ b/InvenTree/build/views.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.shortcuts import get_object_or_404 +from django.http import HttpResponseRedirect + +from django.views.generic import DetailView, ListView +from django.views.generic.edit import UpdateView, DeleteView, CreateView + +from .models import Build + + +class BuildIndex(ListView): + model = Build + template_name = 'build/index.html' + context_object_name = 'builds' + + +class BuildDetail(DetailView): + model = Build + template_name = 'build/detail.html' + context_object_name = 'build' diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index eec6e744e8..ab95b2f43f 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -85,9 +85,7 @@ part_urls = [ url(r'^bom/(?P\d+)/', include(part_bom_urls)), # Top level part list (display top level parts and categories) - url('', views.PartIndex.as_view(), name='part-index'), - - url(r'^.*$', RedirectView.as_view(url='', permanent=False), name='part-index'), + url(r'^.*$', views.PartIndex.as_view(), name='part-index'), ] """ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 57feb04206..0fe775a6b9 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals from django.shortcuts import get_object_or_404 from django.http import HttpResponseRedirect diff --git a/InvenTree/project/admin.py b/InvenTree/project/admin.py deleted file mode 100644 index 4d6ccb520a..0000000000 --- a/InvenTree/project/admin.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.contrib import admin - -from .models import ProjectCategory, Project, ProjectPart, ProjectRun - - -class ProjectCategoryAdmin(admin.ModelAdmin): - list_display = ('name', 'pathstring', 'description') - - -class ProjectAdmin(admin.ModelAdmin): - list_display = ('name', 'description', 'category') - - -class ProjectPartAdmin(admin.ModelAdmin): - list_display = ('part', 'project', 'quantity', 'output') - - -class ProjectRunAdmin(admin.ModelAdmin): - list_display = ('project', 'quantity', 'run_date') - - -admin.site.register(ProjectCategory, ProjectCategoryAdmin) -admin.site.register(Project, ProjectAdmin) -admin.site.register(ProjectPart, ProjectPartAdmin) -admin.site.register(ProjectRun, ProjectRunAdmin) diff --git a/InvenTree/project/migrations/0001_initial.py b/InvenTree/project/migrations/0001_initial.py deleted file mode 100644 index 81158f76e6..0000000000 --- a/InvenTree/project/migrations/0001_initial.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11 on 2018-04-12 05:02 -from __future__ import unicode_literals - -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('part', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Project', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('description', models.CharField(blank=True, max_length=500)), - ], - ), - migrations.CreateModel( - name='ProjectCategory', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('description', models.CharField(blank=True, max_length=250)), - ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='project.ProjectCategory')), - ], - options={ - 'verbose_name': 'Project Category', - 'verbose_name_plural': 'Project Categories', - }, - ), - migrations.CreateModel( - name='ProjectPart', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('quantity', models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(0)])), - ('output', models.BooleanField(default=False)), - ('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='part.Part')), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project.Project')), - ], - ), - migrations.CreateModel( - name='ProjectRun', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('quantity', models.PositiveIntegerField(default=1, validators=[django.core.validators.MinValueValidator(0)])), - ('run_date', models.DateField(blank=True, null=True)), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='project.Project')), - ], - ), - migrations.AddField( - model_name='project', - name='category', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='projects', to='project.ProjectCategory'), - ), - migrations.AlterUniqueTogether( - name='projectpart', - unique_together=set([('part', 'project')]), - ), - migrations.AlterUniqueTogether( - name='project', - unique_together=set([('name', 'category')]), - ), - ] diff --git a/InvenTree/project/models.py b/InvenTree/project/models.py deleted file mode 100644 index a36be41562..0000000000 --- a/InvenTree/project/models.py +++ /dev/null @@ -1,96 +0,0 @@ -from __future__ import unicode_literals -# from django.utils.translation import ugettext as _ - -from django.db import models - -from InvenTree.models import InvenTreeTree -from part.models import Part -from django.core.validators import MinValueValidator - - -class ProjectCategory(InvenTreeTree): - """ ProjectCategory provides hierarchical organization of Project objects. - Each ProjectCategory can contain zero-or-more child categories, - and in turn can have zero-or-one parent category. - """ - - class Meta: - verbose_name = "Project Category" - verbose_name_plural = "Project Categories" - - @property - def projects(self): - return self.project_set.all() - - -class Project(models.Model): - """ A Project takes multiple Part objects. - A project can output zero-or-more Part objects - """ - - name = models.CharField(max_length=100) - description = models.CharField(max_length=500, blank=True) - category = models.ForeignKey(ProjectCategory, on_delete=models.CASCADE, related_name='projects') - - class Meta: - unique_together = ('name', 'category') - - def __str__(self): - return self.name - - -class ProjectPart(models.Model): - """ A project part associates a single part with a project - The quantity of parts required for a single-run of that project is stored. - The overage is the number of extra parts that are generally used for a single run. - """ - - part = models.ForeignKey(Part, on_delete=models.CASCADE) - project = models.ForeignKey(Project, on_delete=models.CASCADE) - quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0)]) - - class Meta: - unique_together = ('part', 'project') - - """ - # TODO - Add overage model fields - - # Overage types - OVERAGE_PERCENT = 0 - OVERAGE_ABSOLUTE = 1 - - OVARAGE_CODES = { - OVERAGE_PERCENT: _("Percent"), - OVERAGE_ABSOLUTE: _("Absolute") - } - - overage = models.FloatField(default=0) - overage_type = models.PositiveIntegerField( - default=OVERAGE_ABSOLUTE, - choices=OVARAGE_CODES.items(), - validators=[MinValueValidator(0)]) - """ - - # Set if the part is generated by the project, - # rather than being consumed by the project - output = models.BooleanField(default=False) - - def __str__(self): - return "{quan} x {name}".format( - name=self.part.name, - quan=self.quantity) - - -class ProjectRun(models.Model): - """ A single run of a particular project. - Tracks the number of 'units' made in the project. - Provides functionality to update stock, - based on both: - a) Parts used (project inputs) - b) Parts produced (project outputs) - """ - - project = models.ForeignKey(Project, on_delete=models.CASCADE) - quantity = models.PositiveIntegerField(default=1, validators=[MinValueValidator(0)]) - - run_date = models.DateField(blank=True, null=True) diff --git a/InvenTree/project/serializers.py b/InvenTree/project/serializers.py deleted file mode 100644 index 9186813457..0000000000 --- a/InvenTree/project/serializers.py +++ /dev/null @@ -1,47 +0,0 @@ -from rest_framework import serializers - -from .models import ProjectCategory, Project, ProjectPart, ProjectRun - - -class ProjectPartSerializer(serializers.HyperlinkedModelSerializer): - - class Meta: - model = ProjectPart - fields = ('url', - 'part', - 'project', - 'quantity', - 'output') - - -class ProjectSerializer(serializers.HyperlinkedModelSerializer): - - class Meta: - model = Project - fields = ('url', - 'name', - 'description', - 'category') - - -class ProjectCategorySerializer(serializers.HyperlinkedModelSerializer): - - class Meta: - model = ProjectCategory - fields = ('url', - 'name', - 'description', - 'parent', - 'pathstring') - - -class ProjectRunSerializer(serializers.HyperlinkedModelSerializer): - - class Meta: - model = ProjectRun - fields = ('url', - 'project', - 'quantity', - 'run_date') - - read_only_fields = ('run_date',) diff --git a/InvenTree/project/tests.py b/InvenTree/project/tests.py deleted file mode 100644 index a79ca8be56..0000000000 --- a/InvenTree/project/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.test import TestCase - -# Create your tests here. diff --git a/InvenTree/project/urls.py b/InvenTree/project/urls.py deleted file mode 100644 index e95a78fd82..0000000000 --- a/InvenTree/project/urls.py +++ /dev/null @@ -1,39 +0,0 @@ -from django.conf.urls import url - -from . import views - -prj_part_urls = [ - # Detail of a single project part - url(r'^(?P[0-9]+)/?$', views.ProjectPartDetail.as_view(), name='projectpart-detail'), - - # List project parts, with optional filters - url(r'^\?.*/?$', views.ProjectPartsList.as_view()), - url(r'^$', views.ProjectPartsList.as_view()) -] - -prj_cat_urls = [ - # Detail of a single project category - url(r'^(?P[0-9]+)/?$', views.ProjectCategoryDetail.as_view(), name='projectcategory-detail'), - - # List of project categories, with filters - url(r'^\?.*/?$', views.ProjectCategoryList.as_view()), - url(r'^$', views.ProjectCategoryList.as_view()) -] - -prj_urls = [ - # Individual project URL - url(r'^(?P[0-9]+)/?$', views.ProjectDetail.as_view(), name='project-detail'), - - # List of all projects - url(r'^\?.*/?$', views.ProjectList.as_view()), - url(r'^$', views.ProjectList.as_view()) -] - -prj_run_urls = [ - # Individual project URL - url(r'^(?P[0-9]+)/?$', views.ProjectRunDetail.as_view(), name='projectrun-detail'), - - # List of all projects - url(r'^\?.*/?$', views.ProjectRunList.as_view()), - url(r'^$', views.ProjectRunList.as_view()) -] diff --git a/InvenTree/project/views.py b/InvenTree/project/views.py deleted file mode 100644 index 4aa12efa2b..0000000000 --- a/InvenTree/project/views.py +++ /dev/null @@ -1,183 +0,0 @@ -from django_filters.rest_framework import FilterSet, DjangoFilterBackend - -from rest_framework import generics, permissions -from InvenTree.models import FilterChildren -from .models import ProjectCategory, Project, ProjectPart, ProjectRun -from .serializers import ProjectSerializer -from .serializers import ProjectCategorySerializer -from .serializers import ProjectPartSerializer -from .serializers import ProjectRunSerializer - - -class ProjectDetail(generics.RetrieveUpdateDestroyAPIView): - """ - - get: - Return a single Project object - - post: - Update a Project - - delete: - Remove a Project - - """ - - queryset = Project.objects.all() - serializer_class = ProjectSerializer - permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - - -class ProjectFilter(FilterSet): - - class Meta: - model = Project - fields = ['category'] - - -class ProjectList(generics.ListCreateAPIView): - """ - - get: - Return a list of all Project objects - (with optional query filters) - - post: - Create a new Project - - """ - - queryset = Project.objects.all() - serializer_class = ProjectSerializer - permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - filter_backends = (DjangoFilterBackend,) - filter_class = ProjectFilter - - -class ProjectCategoryDetail(generics.RetrieveUpdateAPIView): - """ - - get: - Return a single ProjectCategory object - - post: - Update a ProjectCategory - - delete: - Remove a ProjectCategory - - """ - - queryset = ProjectCategory.objects.all() - serializer_class = ProjectCategorySerializer - permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - - -class ProjectCategoryList(generics.ListCreateAPIView): - """ - - get: - Return a list of all ProjectCategory objects - - post: - Create a new ProjectCategory - - """ - - def get_queryset(self): - params = self.request.query_params - - categories = ProjectCategory.objects.all() - - categories = FilterChildren(categories, params.get('parent', None)) - - return categories - - serializer_class = ProjectCategorySerializer - permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - - -class ProjectPartFilter(FilterSet): - - class Meta: - model = ProjectPart - fields = ['project', 'part'] - - -class ProjectPartsList(generics.ListCreateAPIView): - """ - - get: - Return a list of all ProjectPart objects - - post: - Create a new ProjectPart - - """ - - serializer_class = ProjectPartSerializer - permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - queryset = ProjectPart.objects.all() - filter_backends = (DjangoFilterBackend,) - filter_class = ProjectPartFilter - - -class ProjectPartDetail(generics.RetrieveUpdateDestroyAPIView): - """ - - get: - Return a single ProjectPart object - - post: - Update a ProjectPart - - delete: - Remove a ProjectPart - - """ - - queryset = ProjectPart.objects.all() - serializer_class = ProjectPartSerializer - permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - - -class ProjectRunDetail(generics.RetrieveUpdateDestroyAPIView): - """ - - get: - Return a single ProjectRun - - post: - Update a ProjectRun - - delete: - Remove a ProjectRun - """ - - queryset = ProjectRun.objects.all() - serializer_class = ProjectRunSerializer - permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - - -class ProjectRunFilter(FilterSet): - - class Meta: - model = ProjectRun - fields = ['project'] - - -class ProjectRunList(generics.ListCreateAPIView): - """ - - get: - Return a list of all ProjectRun objects - - post: - Create a new ProjectRun object - """ - - queryset = ProjectRun.objects.all() - serializer_class = ProjectRunSerializer - permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - filter_backends = (DjangoFilterBackend,) - filter_class = ProjectRunFilter diff --git a/InvenTree/stock/migrations/0010_stockitem_build.py b/InvenTree/stock/migrations/0010_stockitem_build.py new file mode 100644 index 0000000000..831fc72e25 --- /dev/null +++ b/InvenTree/stock/migrations/0010_stockitem_build.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2018-04-16 14:03 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0001_initial'), + ('stock', '0009_auto_20180416_1253'), + ] + + operations = [ + migrations.AddField( + model_name='stockitem', + name='build', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='build.Build'), + ), + ] diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index b48514976b..358d11e1d8 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -3,16 +3,16 @@ from django.utils.translation import ugettext as _ from django.db import models, transaction from django.core.validators import MinValueValidator from django.contrib.auth.models import User +from django.db.models.signals import pre_delete +from django.dispatch import receiver + +from datetime import datetime from supplier.models import SupplierPart from supplier.models import Customer from part.models import Part from InvenTree.models import InvenTreeTree - -from datetime import datetime - -from django.db.models.signals import pre_delete -from django.dispatch import receiver +from build.models import Build class StockLocation(InvenTreeTree): @@ -38,13 +38,8 @@ def before_delete_stock_location(sender, instance, using, **kwargs): # Update each part in the stock location for item in instance.items.all(): - # If this location has a parent, move the child stock items to the parent - if instance.parent: - item.location = instance.parent - item.save() - # No parent location? Delete the stock items - else: - item.delete() + item.location = instance.parent + item.save() # Update each child category for child in instance.children.all(): @@ -100,6 +95,9 @@ class StockItem(models.Model): batch = models.CharField(max_length=100, blank=True, help_text='Batch code for this stock item') + # If this part was produced by a build, point to that build here + build = models.ForeignKey(Build, on_delete=models.SET_NULL, blank=True, null=True) + # Quantity of this stock item. Value may be overridden by other settings quantity = models.PositiveIntegerField(validators=[MinValueValidator(0)]) @@ -113,7 +111,6 @@ class StockItem(models.Model): review_needed = models.BooleanField(default=False) - # Stock status types ITEM_OK = 10 ITEM_ATTENTION = 50 ITEM_DAMAGED = 55 diff --git a/InvenTree/stock/templates/stock/location_delete.html b/InvenTree/stock/templates/stock/location_delete.html index 135ed55d51..5bdd5249f6 100644 --- a/InvenTree/stock/templates/stock/location_delete.html +++ b/InvenTree/stock/templates/stock/location_delete.html @@ -11,7 +11,7 @@ If this location is deleted, these child locations will be moved to {% if location.parent %} the '{{ location.parent.name }}' location. {% else %} -the top level 'Stock' category. +the top level 'Stock' location. {% endif %}

@@ -27,7 +27,7 @@ the top level 'Stock' category. {% if location.parent %} If this location is deleted, these items will be moved to the '{{ location.parent.name }}' location. {% else %} -If this location is deleted, these items will be deleted! +If this location is deleted, these items will be moved to the top level 'Stock' location. {% endif %}

diff --git a/Makefile b/Makefile index e2a51d7779..9420b60374 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ migrate: python InvenTree/manage.py makemigrations part python InvenTree/manage.py makemigrations stock python InvenTree/manage.py makemigrations supplier + python InvenTree/manage.py makemigrations build python InvenTree/manage.py migrate --run-syncdb python InvenTree/manage.py check