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