diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index 26303a6549..04d1978745 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -199,7 +199,7 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
form = self.get_form()
- return self.renderJsonResponse(request, form)
+ return self.renderJsonResponse(request, form, context=self.get_context_data())
def post(self, request, *args, **kwargs):
""" Respond to POST request.
diff --git a/InvenTree/build/admin.py b/InvenTree/build/admin.py
index 9662f6bf79..0ef5ccb75b 100644
--- a/InvenTree/build/admin.py
+++ b/InvenTree/build/admin.py
@@ -2,21 +2,33 @@
from __future__ import unicode_literals
from django.contrib import admin
+from import_export.admin import ImportExportModelAdmin
-from .models import Build
+from .models import Build, BuildItem
-class BuildAdmin(admin.ModelAdmin):
+class BuildAdmin(ImportExportModelAdmin):
- list_display = ('part',
- 'status',
- 'batch',
- 'quantity',
- 'creation_date',
- 'completion_date',
- 'title',
- 'notes',
- )
+ list_display = (
+ 'part',
+ 'status',
+ 'batch',
+ 'quantity',
+ 'creation_date',
+ 'completion_date',
+ 'title',
+ 'notes',
+ )
+
+
+class BuildItemAdmin(admin.ModelAdmin):
+
+ list_display = (
+ 'build',
+ 'stock_item',
+ 'quantity'
+ )
admin.site.register(Build, BuildAdmin)
+admin.site.register(BuildItem, BuildItemAdmin)
diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py
index 393bd1f073..8e0a92917e 100644
--- a/InvenTree/build/api.py
+++ b/InvenTree/build/api.py
@@ -9,10 +9,10 @@ from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
from rest_framework import generics, permissions
-from django.conf.urls import url
+from django.conf.urls import url, include
-from .models import Build
-from .serializers import BuildSerializer
+from .models import Build, BuildItem
+from .serializers import BuildSerializer, BuildItemSerializer
class BuildList(generics.ListCreateAPIView):
@@ -40,6 +40,50 @@ class BuildList(generics.ListCreateAPIView):
]
-build_api_urls = [
- url(r'^.*$', BuildList.as_view(), name='api-build-list')
+class BuildItemList(generics.ListCreateAPIView):
+ """ API endpoint for accessing a list of BuildItem objects
+
+ - GET: Return list of objects
+ - POST: Create a new BuildItem object
+ """
+
+ serializer_class = BuildItemSerializer
+
+ def get_queryset(self):
+ """ Override the queryset method,
+ to allow filtering by stock_item.part
+ """
+
+ # Does the user wish to filter by part?
+ part_pk = self.request.query_params.get('part', None)
+
+ query = BuildItem.objects.all()
+
+ if part_pk:
+ query = query.filter(stock_item__part=part_pk)
+
+ return query
+
+ permission_classes = [
+ permissions.IsAuthenticatedOrReadOnly,
+ ]
+
+ filter_backends = [
+ DjangoFilterBackend,
+ ]
+
+ filter_fields = [
+ 'build',
+ 'stock_item'
+ ]
+
+
+build_item_api_urls = [
+ url('^.*$', BuildItemList.as_view(), name='api-build-item-list'),
+]
+
+build_api_urls = [
+ url(r'^item/?', include(build_item_api_urls)),
+
+ url(r'^.*$', BuildList.as_view(), name='api-build-list'),
]
diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py
index 6254f0745c..de56ca34f7 100644
--- a/InvenTree/build/forms.py
+++ b/InvenTree/build/forms.py
@@ -6,8 +6,9 @@ Django Forms for interacting with Build objects
from __future__ import unicode_literals
from InvenTree.forms import HelperForm
-
-from .models import Build
+from django import forms
+from .models import Build, BuildItem
+from stock.models import StockLocation
class EditBuildForm(HelperForm):
@@ -21,7 +22,36 @@ class EditBuildForm(HelperForm):
'part',
'quantity',
'batch',
+ 'URL',
'notes',
- # 'status',
- # 'completion_date',
+ ]
+
+
+class CompleteBuildForm(HelperForm):
+ """ Form for marking a Build as complete """
+
+ location = forms.ModelChoiceField(
+ queryset=StockLocation.objects.all(),
+ help_text='Location of completed parts',
+ )
+
+ confirm = forms.BooleanField(required=False, help_text='Confirm build submission')
+
+ class Meta:
+ model = Build
+ fields = [
+ 'location',
+ 'confirm'
+ ]
+
+
+class EditBuildItemForm(HelperForm):
+ """ Form for adding a new BuildItem to a Build """
+
+ class Meta:
+ model = BuildItem
+ fields = [
+ 'build',
+ 'stock_item',
+ 'quantity',
]
diff --git a/InvenTree/build/migrations/0003_builditemallocation.py b/InvenTree/build/migrations/0003_builditemallocation.py
new file mode 100644
index 0000000000..add13c7ac1
--- /dev/null
+++ b/InvenTree/build/migrations/0003_builditemallocation.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.2 on 2019-04-29 12:14
+
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stock', '0009_auto_20190428_0841'),
+ ('build', '0002_auto_20190412_2030'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='BuildItemAllocation',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('quantity', models.PositiveIntegerField(default=1, help_text='Stock quantity to allocate to build', validators=[django.core.validators.MinValueValidator(1)])),
+ ('build', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allocated_stock', to='build.Build')),
+ ('stock', models.ForeignKey(help_text='Stock Item to allocate to build', on_delete=django.db.models.deletion.CASCADE, related_name='allocations', to='stock.StockItem')),
+ ],
+ ),
+ ]
diff --git a/InvenTree/build/migrations/0004_build_url.py b/InvenTree/build/migrations/0004_build_url.py
new file mode 100644
index 0000000000..187ac938d1
--- /dev/null
+++ b/InvenTree/build/migrations/0004_build_url.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2 on 2019-04-29 12:18
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('build', '0003_builditemallocation'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='build',
+ name='URL',
+ field=models.URLField(blank=True, help_text='Link to external URL'),
+ ),
+ ]
diff --git a/InvenTree/build/migrations/0005_auto_20190429_2229.py b/InvenTree/build/migrations/0005_auto_20190429_2229.py
new file mode 100644
index 0000000000..73ebca7e1b
--- /dev/null
+++ b/InvenTree/build/migrations/0005_auto_20190429_2229.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2 on 2019-04-29 12:29
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stock', '0009_auto_20190428_0841'),
+ ('build', '0004_build_url'),
+ ]
+
+ operations = [
+ migrations.RenameModel(
+ old_name='BuildItemAllocation',
+ new_name='BuildItem',
+ ),
+ ]
diff --git a/InvenTree/build/migrations/0006_auto_20190429_2233.py b/InvenTree/build/migrations/0006_auto_20190429_2233.py
new file mode 100644
index 0000000000..5d03e13b75
--- /dev/null
+++ b/InvenTree/build/migrations/0006_auto_20190429_2233.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2 on 2019-04-29 12:33
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('build', '0005_auto_20190429_2229'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='builditem',
+ old_name='stock',
+ new_name='stock_item',
+ ),
+ ]
diff --git a/InvenTree/build/migrations/0007_auto_20190429_2255.py b/InvenTree/build/migrations/0007_auto_20190429_2255.py
new file mode 100644
index 0000000000..8a01606b84
--- /dev/null
+++ b/InvenTree/build/migrations/0007_auto_20190429_2255.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2 on 2019-04-29 12:55
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stock', '0009_auto_20190428_0841'),
+ ('build', '0006_auto_20190429_2233'),
+ ]
+
+ operations = [
+ migrations.AlterUniqueTogether(
+ name='builditem',
+ unique_together={('build', 'stock_item')},
+ ),
+ ]
diff --git a/InvenTree/build/migrations/0008_auto_20190501_2344.py b/InvenTree/build/migrations/0008_auto_20190501_2344.py
new file mode 100644
index 0000000000..febdd2d1b1
--- /dev/null
+++ b/InvenTree/build/migrations/0008_auto_20190501_2344.py
@@ -0,0 +1,25 @@
+# Generated by Django 2.2 on 2019-05-01 13:44
+
+import django.core.validators
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('build', '0007_auto_20190429_2255'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='build',
+ name='status',
+ field=models.PositiveIntegerField(choices=[(10, 'Pending'), (30, 'Cancelled'), (40, 'Complete')], default=10, validators=[django.core.validators.MinValueValidator(0)]),
+ ),
+ migrations.AlterField(
+ model_name='builditem',
+ name='build',
+ field=models.ForeignKey(help_text='Build to allocate parts', on_delete=django.db.models.deletion.CASCADE, related_name='allocated_stock', to='build.Build'),
+ ),
+ ]
diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py
index 880c1b08b1..79bedd5616 100644
--- a/InvenTree/build/models.py
+++ b/InvenTree/build/models.py
@@ -5,17 +5,79 @@ Build database model definitions
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
+from datetime import datetime
+
from django.utils.translation import ugettext as _
+from django.core.exceptions import ValidationError
from django.urls import reverse
-from django.db import models
+from django.db import models, transaction
from django.core.validators import MinValueValidator
+from stock.models import StockItem
+
class Build(models.Model):
""" A Build object organises the creation of new parts from the component parts.
+
+ Attributes:
+ part: The part to be built (from component BOM items)
+ title: Brief title describing the build (required)
+ quantity: Number of units to be built
+ status: Build status code
+ batch: Batch code transferred to build parts (optional)
+ creation_date: Date the build was created (auto)
+ completion_date: Date the build was completed
+ URL: External URL for extra information
+ notes: Text notes
"""
+ def save(self, *args, **kwargs):
+ """ Called when the Build model is saved to the database.
+
+ If this is a new Build, try to allocate StockItem objects automatically.
+
+ - If there is only one StockItem for a Part, use that one.
+ - If there are multiple StockItem objects, leave blank and let the user decide
+ """
+
+ allocate_parts = False
+
+ # If there is no PK yet, then this is the first time the Build has been saved
+ if not self.pk:
+ allocate_parts = True
+
+ # Save this Build first
+ super(Build, self).save(*args, **kwargs)
+
+ if allocate_parts:
+ for item in self.part.bom_items.all():
+ part = item.sub_part
+ # Number of parts required for this build
+ q_req = item.quantity * self.quantity
+
+ stock = StockItem.objects.filter(part=part)
+
+ if len(stock) == 1:
+ stock_item = stock[0]
+
+ # Are there any parts available?
+ if stock_item.quantity > 0:
+ # If there are not enough parts, reduce the amount we will take
+ if stock_item.quantity < q_req:
+ q_req = stock_item.quantity
+
+ # Allocate parts to this build
+ build_item = BuildItem(
+ build=self,
+ stock_item=stock_item,
+ quantity=q_req)
+
+ build_item.save()
+
+ def __str__(self):
+ return "Build {q} x {part}".format(q=self.quantity, part=str(self.part))
+
def get_absolute_url(self):
return reverse('build-detail', kwargs={'pk': self.id})
@@ -23,46 +85,101 @@ class Build(models.Model):
related_name='builds',
limit_choices_to={'buildable': True},
)
- """ A reference to the part being built - only parts marked as 'buildable' may be selected """
- #: Brief title describing the build
title = models.CharField(max_length=100, help_text='Brief description of the build')
- #: Number of output parts to build
- quantity = models.PositiveIntegerField(default=1,
- validators=[MinValueValidator(1)],
- help_text='Number of parts to build')
-
+ quantity = models.PositiveIntegerField(
+ default=1,
+ validators=[MinValueValidator(1)],
+ help_text='Number of parts to build'
+ )
+
# Build status codes
PENDING = 10 # Build is pending / active
- HOLDING = 20 # Build is currently being held
CANCELLED = 30 # Build was cancelled
COMPLETE = 40 # Build is complete
#: Build status codes
BUILD_STATUS_CODES = {PENDING: _("Pending"),
- HOLDING: _("Holding"),
CANCELLED: _("Cancelled"),
COMPLETE: _("Complete"),
}
- #: Status of the build (ref BUILD_STATUS_CODES)
status = models.PositiveIntegerField(default=PENDING,
choices=BUILD_STATUS_CODES.items(),
validators=[MinValueValidator(0)])
-
- #: Batch number for the build (optional)
+
batch = models.CharField(max_length=100, blank=True, null=True,
help_text='Batch code for this build output')
-
- #: Date the build model was 'created'
+
creation_date = models.DateField(auto_now=True, editable=False)
-
- #: Date the build was 'completed' (and parts removed from stock)
+
completion_date = models.DateField(null=True, blank=True)
+
+ URL = models.URLField(blank=True, help_text='Link to external URL')
- #: Notes attached to each build output
notes = models.TextField(blank=True)
+ """ Notes attached to each build output """
+
+ @transaction.atomic
+ def cancelBuild(self):
+ """ Mark the Build as CANCELLED
+
+ - Delete any pending BuildItem objects (but do not remove items from stock)
+ - Set build status to CANCELLED
+ - Save the Build object
+ """
+
+ for item in self.allocated_stock.all():
+ item.delete()
+
+ self.status = self.CANCELLED
+ self.save()
+
+ @transaction.atomic
+ def completeBuild(self, location, user):
+ """ Mark the Build as COMPLETE
+
+ - Takes allocated items from stock
+ - Delete pending BuildItem objects
+ """
+
+ for item in self.allocated_stock.all():
+
+ # Subtract stock from the item
+ item.stock_item.take_stock(
+ item.quantity,
+ user,
+ 'Removed {n} items to build {m} x {part}'.format(
+ n=item.quantity,
+ m=self.quantity,
+ part=self.part.name
+ )
+ )
+
+ # Delete the item
+ item.delete()
+
+ # Mark the date of completion
+ self.completion_date = datetime.now().date()
+
+ # Add stock of the newly created item
+ item = StockItem.objects.create(
+ part=self.part,
+ location=location,
+ quantity=self.quantity,
+ batch=str(self.batch) if self.batch else '',
+ notes='Built {q} on {now}'.format(
+ q=self.quantity,
+ now=str(datetime.now().date())
+ )
+ )
+
+ item.save()
+
+ # Finally, mark the build as complete
+ self.status = self.COMPLETE
+ self.save()
@property
def required_parts(self):
@@ -99,10 +216,77 @@ class Build(models.Model):
return self.status in [
self.PENDING,
- self.HOLDING
]
@property
def is_complete(self):
""" Returns True if the build status is COMPLETE """
return self.status == self.COMPLETE
+
+
+class BuildItem(models.Model):
+ """ A BuildItem links multiple StockItem objects to a Build.
+ These are used to allocate part stock to a build.
+ Once the Build is completed, the parts are removed from stock and the
+ BuildItemAllocation objects are removed.
+
+ Attributes:
+ build: Link to a Build object
+ stock: Link to a StockItem object
+ quantity: Number of units allocated
+ """
+
+ def get_absolute_url(self):
+ # TODO - Fix!
+ return '/build/item/{pk}/'.format(pk=self.id)
+ # return reverse('build-detail', kwargs={'pk': self.id})
+
+ class Meta:
+ unique_together = [
+ ('build', 'stock_item'),
+ ]
+
+ def clean(self):
+ """ Check validity of the BuildItem model.
+ The following checks are performed:
+
+ - StockItem.part must be in the BOM of the Part object referenced by Build
+ - Allocation quantity cannot exceed available quantity
+ """
+
+ super(BuildItem, self).clean()
+
+ errors = {}
+
+ if self.stock_item is not None and self.stock_item.part is not None:
+ if self.stock_item.part not in self.build.part.required_parts():
+ errors['stock_item'] = [_("Selected stock item not found in BOM for part '{p}'".format(p=self.build.part.name))]
+
+ if self.stock_item is not None and self.quantity > self.stock_item.quantity:
+ errors['quantity'] = [_("Allocated quantity ({n}) must not exceed available quantity ({q})".format(
+ n=self.quantity,
+ q=self.stock_item.quantity
+ ))]
+
+ if len(errors) > 0:
+ raise ValidationError(errors)
+
+ build = models.ForeignKey(
+ Build,
+ on_delete=models.CASCADE,
+ related_name='allocated_stock',
+ help_text='Build to allocate parts'
+ )
+
+ stock_item = models.ForeignKey(
+ 'stock.StockItem',
+ on_delete=models.CASCADE,
+ related_name='allocations',
+ help_text='Stock Item to allocate to build',
+ )
+
+ quantity = models.PositiveIntegerField(
+ default=1,
+ validators=[MinValueValidator(1)],
+ help_text='Stock quantity to allocate to build'
+ )
diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py
index 955a8d59f5..947f3f6dc1 100644
--- a/InvenTree/build/serializers.py
+++ b/InvenTree/build/serializers.py
@@ -6,11 +6,13 @@ JSON serializers for Build API
from __future__ import unicode_literals
from rest_framework import serializers
+from InvenTree.serializers import InvenTreeModelSerializer
+from stock.serializers import StockItemSerializerBrief
-from .models import Build
+from .models import Build, BuildItem
-class BuildSerializer(serializers.ModelSerializer):
+class BuildSerializer(InvenTreeModelSerializer):
""" Serializes a Build object """
url = serializers.CharField(source='get_absolute_url', read_only=True)
@@ -29,3 +31,30 @@ class BuildSerializer(serializers.ModelSerializer):
'status',
'status_text',
'notes']
+
+ read_only_fields = [
+ 'status',
+ 'creation_date',
+ 'completion_data',
+ 'status_text',
+ ]
+
+
+class BuildItemSerializer(InvenTreeModelSerializer):
+ """ Serializes a BuildItem object """
+
+ part = serializers.IntegerField(source='stock_item.part.pk', read_only=True)
+ part_name = serializers.CharField(source='stock_item.part.name', read_only=True)
+ stock_item_detail = StockItemSerializerBrief(source='stock_item', read_only=True)
+
+ class Meta:
+ model = BuildItem
+ fields = [
+ 'pk',
+ 'build',
+ 'part',
+ 'part_name',
+ 'stock_item',
+ 'stock_item_detail',
+ 'quantity'
+ ]
diff --git a/InvenTree/build/templates/build/allocate.html b/InvenTree/build/templates/build/allocate.html
index 0a746524b3..c3cda38429 100644
--- a/InvenTree/build/templates/build/allocate.html
+++ b/InvenTree/build/templates/build/allocate.html
@@ -1,47 +1,59 @@
{% extends "base.html" %}
{% load static %}
+{% load inventree_extras %}
{% block content %}
Allocate Parts for Build
-
+{{ build.quantity }} x {{ build.part.name }}
+{% for bom_item in bom_items.all %}
+{% include "build/allocation_item.html" with item=bom_item build=build %}
+{% endfor %}
+
+
+ Complete Build
+
+
{% endblock %}
{% block js_load %}
{{ block.super }}
+
{% endblock %}
{% block js_ready %}
{{ block.super }}
- $('#build-table').bootstrapTable({
- sortable: true,
- columns: [
+ {% for bom_item in bom_items.all %}
+
+ loadAllocationTable(
+ $("#allocate-table-id-{{ bom_item.sub_part.id }}"),
+ {{ bom_item.sub_part.id }},
+ "{{ bom_item.sub_part.name }}",
+ "{% url 'api-build-item-list' %}?build={{ build.id }}&part={{ bom_item.sub_part.id }}",
+ {% multiply build.quantity bom_item.quantity %},
+ $("#new-item-{{ bom_item.sub_part.id }}")
+ );
+
+ {% endfor %}
+
+ $("#complete-build").on('click', function() {
+ launchModalForm(
+ "{% url 'build-complete' build.id %}",
{
- field: 'sub_part.name',
- title: 'Part',
- },
- {
- title: 'Source',
- },
- {
- field: 'quantity',
- title: 'Quantity',
+ reload: true,
+ submit_text: "Complete Build",
}
- ],
+ );
});
- getBomList({part: {{ build.part.id }}}).then(function(response) {
- $("#build-table").bootstrapTable('load', response);
- });
-
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/InvenTree/build/templates/build/allocation_item.html b/InvenTree/build/templates/build/allocation_item.html
new file mode 100644
index 0000000000..9982457aca
--- /dev/null
+++ b/InvenTree/build/templates/build/allocation_item.html
@@ -0,0 +1,38 @@
+{% load inventree_extras %}
+
+
+
+
+
+
+
+ Required:
+
+
+ {% multiply build.quantity item.quantity %}
+
+
+ Allocated:
+
+
+ 0
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/InvenTree/build/templates/build/complete.html b/InvenTree/build/templates/build/complete.html
new file mode 100644
index 0000000000..506f766c44
--- /dev/null
+++ b/InvenTree/build/templates/build/complete.html
@@ -0,0 +1,24 @@
+{% extends "modal_form.html" %}
+
+{% block pre_form_content %}
+Build: {{ build.title }} - {{ build.quantity }} x {{ build.part.name }}
+
+Are you sure you want to mark this build as complete?
+
+{% if taking %}
+The following items will be removed from stock:
+
+{% for item in taking %}
+ {{ item.quantity }} x {{ item.stock_item.part.name }} from {{ item.stock_item.location }}
+{% endfor %}
+
+{% else %}
+No parts have been allocated to this build.
+{% endif %}
+
+The following items will be created:
+
+ {{ build.quantity }} x {{ build.part.name }}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/build/templates/build/delete_build_item.html b/InvenTree/build/templates/build/delete_build_item.html
new file mode 100644
index 0000000000..23993feb95
--- /dev/null
+++ b/InvenTree/build/templates/build/delete_build_item.html
@@ -0,0 +1,3 @@
+Are you sure you want to unallocate these parts?
+
+This will remove {{ item.quantity }} parts from build '{{ item.build.title }}'.
\ No newline at end of file
diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html
index ad25b4307d..1960ed4ef5 100644
--- a/InvenTree/build/templates/build/detail.html
+++ b/InvenTree/build/templates/build/detail.html
@@ -38,14 +38,19 @@
Quantity {{ build.quantity }}
+
+ Status {% include "build_status.html" with build=build %}
+
{% if build.batch %}
Batch {{ build.batch }}
{% endif %}
+{% if build.URL %}
- Status {% include "build_status.html" with build=build %}
+ URL {{ build.URL }}
+{% endif %}
Created {{ build.creation_date }}
@@ -73,6 +78,7 @@
{% endif %}
+{% if build.is_active %}
Required Parts
+{% endif %}
+
{% include 'modals.html' %}
{% endblock %}
@@ -110,7 +118,7 @@
launchModalForm("{% url 'build-cancel' build.id %}",
{
reload: true,
- submit_text: "Cancel",
+ submit_text: "Cancel Build",
});
});
{% endblock %}
diff --git a/InvenTree/build/templatetags/__init__.py b/InvenTree/build/templatetags/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/InvenTree/build/templatetags/inventree_extras.py b/InvenTree/build/templatetags/inventree_extras.py
new file mode 100644
index 0000000000..cb8cd0d235
--- /dev/null
+++ b/InvenTree/build/templatetags/inventree_extras.py
@@ -0,0 +1,12 @@
+""" This module provides template tags for extra functionality
+over and above the built-in Django tags.
+"""
+
+from django import template
+
+register = template.Library()
+
+
+@register.simple_tag()
+def multiply(x, y, *args, **kwargs):
+ return x * y
diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py
index fea3b6596c..162b9a613f 100644
--- a/InvenTree/build/urls.py
+++ b/InvenTree/build/urls.py
@@ -6,16 +6,29 @@ from django.conf.urls import url, include
from . import views
+build_item_detail_urls = [
+ url('^edit/?', views.BuildItemEdit.as_view(), name='build-item-edit'),
+ url('^delete/?', views.BuildItemDelete.as_view(), name='build-item-delete'),
+]
+
+build_item_urls = [
+ url(r'^(?P\d+)/', include(build_item_detail_urls)),
+ url('^new/', views.BuildItemCreate.as_view(), name='build-item-create'),
+]
+
build_detail_urls = [
url(r'^edit/?', views.BuildUpdate.as_view(), name='build-edit'),
url(r'^allocate/?', views.BuildAllocate.as_view(), name='build-allocate'),
url(r'^cancel/?', views.BuildCancel.as_view(), name='build-cancel'),
+ url(r'^complete/?', views.BuildComplete.as_view(), name='build-complete'),
url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
]
build_urls = [
- url(r'new/?', views.BuildCreate.as_view(), name='build-create'),
+ url(r'item/', include(build_item_urls)),
+
+ url(r'new/', views.BuildCreate.as_view(), name='build-create'),
url(r'^(?P\d+)/', include(build_detail_urls)),
diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py
index dc7b3141c3..f7de8a3a54 100644
--- a/InvenTree/build/views.py
+++ b/InvenTree/build/views.py
@@ -8,12 +8,14 @@ from __future__ import unicode_literals
from django.shortcuts import get_object_or_404
from django.views.generic import DetailView, ListView
+from django.forms import HiddenInput
from part.models import Part
-from .models import Build
-from .forms import EditBuildForm
+from .models import Build, BuildItem
+from .forms import EditBuildForm, EditBuildItemForm, CompleteBuildForm
+from stock.models import StockLocation, StockItem
-from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView
+from InvenTree.views import AjaxView, AjaxUpdateView, AjaxCreateView, AjaxDeleteView
class BuildIndex(ListView):
@@ -46,7 +48,7 @@ class BuildCancel(AjaxView):
Provides a cancellation information dialog
"""
model = Build
- template_name = 'build/cancel.html'
+ ajax_template_name = 'build/cancel.html'
ajax_form_title = 'Cancel Build'
context_object_name = 'build'
fields = []
@@ -56,15 +58,109 @@ class BuildCancel(AjaxView):
build = get_object_or_404(Build, pk=self.kwargs['pk'])
- build.status = Build.CANCELLED
- build.save()
+ build.cancelBuild()
return self.renderJsonResponse(request, None)
def get_data(self):
""" Provide JSON context data. """
return {
- 'info': 'Build was cancelled'
+ 'danger': 'Build was cancelled'
+ }
+
+
+class BuildComplete(AjaxUpdateView):
+ """ View to mark a build as Complete.
+
+ - Notifies the user of which parts will be removed from stock.
+ - Removes allocated items from stock
+ - Deletes pending BuildItem objects
+ """
+
+ model = Build
+ form_class = CompleteBuildForm
+ context_object_name = "build"
+ ajax_form_title = "Complete Build"
+ ajax_template_name = "build/complete.html"
+
+ def get_initial(self):
+ """ Get initial form data for the CompleteBuild form
+
+ - If the part being built has a default location, pre-select that location
+ """
+
+ initials = super(BuildComplete, self).get_initial().copy()
+
+ build = self.get_object()
+ if build.part.default_location is not None:
+ try:
+ location = StockLocation.objects.get(pk=build.part.default_location.id)
+ initials['location'] = location
+ except StockLocation.DoesNotExist:
+ pass
+
+ return initials
+
+ def get_context_data(self, **kwargs):
+ """ Get context data for passing to the rendered form
+
+ - Build information is required
+ """
+
+ build = self.get_object()
+
+ # Build object
+ context = super(BuildComplete, self).get_context_data(**kwargs).copy()
+ context['build'] = build
+
+ # Items to be removed from stock
+ taking = BuildItem.objects.filter(build=build.id)
+ context['taking'] = taking
+
+ return context
+
+ def post(self, request, *args, **kwargs):
+ """ Handle POST request. Mark the build as COMPLETE
+
+ - If the form validation passes, the Build objects completeBuild() method is called
+ - Otherwise, the form is passed back to the client
+ """
+
+ build = self.get_object()
+
+ form = self.get_form()
+
+ confirm = request.POST.get('confirm', False)
+
+ loc_id = request.POST.get('location', None)
+
+ valid = False
+
+ if confirm is False:
+ form.errors['confirm'] = [
+ 'Confirm completion of build',
+ ]
+ else:
+ try:
+ location = StockLocation.objects.get(id=loc_id)
+ valid = True
+ except StockLocation.DoesNotExist:
+ print('id:', loc_id)
+ form.errors['location'] = ['Invalid location selected']
+
+ if valid:
+ build.completeBuild(location, request.user)
+
+ data = {
+ 'form_valid': valid,
+ }
+
+ return self.renderJsonResponse(request, form, data)
+
+ def get_data(self):
+ """ Provide feedback data back to the form """
+ return {
+ 'info': 'Build marked as COMPLETE'
}
@@ -81,6 +177,20 @@ class BuildAllocate(DetailView):
context_object_name = 'build'
template_name = 'build/allocate.html'
+ def get_context_data(self, **kwargs):
+ """ Provide extra context information for the Build allocation page """
+
+ context = super(DetailView, self).get_context_data(**kwargs)
+
+ build = self.get_object()
+ part = build.part
+ bom_items = part.bom_items
+
+ context['part'] = part
+ context['bom_items'] = bom_items
+
+ return context
+
class BuildCreate(AjaxCreateView):
""" View to create a new Build object """
@@ -127,3 +237,116 @@ class BuildUpdate(AjaxUpdateView):
return {
'info': 'Edited build',
}
+
+
+class BuildItemDelete(AjaxDeleteView):
+ """ View to 'unallocate' a BuildItem.
+ Really we are deleting the BuildItem object from the database.
+ """
+
+ model = BuildItem
+ ajax_template_name = 'build/delete_build_item.html'
+ ajax_form_title = 'Unallocate Stock'
+ context_object_name = 'item'
+
+ def get_data(self):
+ return {
+ 'danger': 'Removed parts from build allocation'
+ }
+
+
+class BuildItemCreate(AjaxCreateView):
+ """ View for allocating a new part to a build """
+
+ model = BuildItem
+ form_class = EditBuildItemForm
+ ajax_template_name = 'modal_form.html'
+ ajax_form_title = 'Allocate new Part'
+
+ def get_form(self):
+ """ Create Form for making / editing new Part object """
+
+ form = super(AjaxCreateView, self).get_form()
+
+ # If the Build object is specified, hide the input field.
+ # We do not want the users to be able to move a BuildItem to a different build
+ build_id = form['build'].value()
+
+ if build_id is not None:
+ form.fields['build'].widget = HiddenInput()
+
+ # If the sub_part is supplied, limit to matching stock items
+ part_id = self.get_param('part')
+
+ if part_id:
+ try:
+ Part.objects.get(pk=part_id)
+
+ query = form.fields['stock_item'].queryset
+
+ # Only allow StockItem objects which match the current part
+ query = query.filter(part=part_id)
+
+ if build_id is not None:
+ # Exclude StockItem objects which are already allocated to this build and part
+ query = query.exclude(id__in=[item.stock_item.id for item in BuildItem.objects.filter(build=build_id, stock_item__part=part_id)])
+
+ form.fields['stock_item'].queryset = query
+ except Part.DoesNotExist:
+ pass
+
+ return form
+
+ def get_initial(self):
+ """ Provide initial data for BomItem. Look for the folllowing in the GET data:
+
+ - build: pk of the Build object
+ """
+
+ initials = super(AjaxCreateView, self).get_initial().copy()
+
+ build_id = self.get_param('build')
+
+ if build_id:
+ try:
+ initials['build'] = Build.objects.get(pk=build_id)
+ except Build.DoesNotExist:
+ pass
+
+ return initials
+
+
+class BuildItemEdit(AjaxUpdateView):
+ """ View to edit a BuildItem object """
+
+ model = BuildItem
+ ajax_template_name = 'modal_form.html'
+ form_class = EditBuildItemForm
+ ajax_form_title = 'Edit Stock Allocation'
+
+ def get_data(self):
+ return {
+ 'info': 'Updated Build Item',
+ }
+
+ def get_form(self):
+ """ Create form for editing a BuildItem.
+
+ - Limit the StockItem options to items that match the part
+ """
+
+ build_item = self.get_object()
+
+ form = super(BuildItemEdit, self).get_form()
+
+ query = StockItem.objects.all()
+
+ if build_item.stock_item:
+ part_id = build_item.stock_item.part.id
+ query = query.filter(part=part_id)
+
+ form.fields['stock_item'].queryset = query
+
+ form.fields['build'].widget = HiddenInput()
+
+ return form
diff --git a/InvenTree/company/templates/company/tabs.html b/InvenTree/company/templates/company/tabs.html
index 7c8c03d484..df8b9a4fc9 100644
--- a/InvenTree/company/templates/company/tabs.html
+++ b/InvenTree/company/templates/company/tabs.html
@@ -19,4 +19,4 @@
{% endif %}
{% endif %}
-
\ No newline at end of file
+
diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py
index 4e63bf65e0..56bf290739 100644
--- a/InvenTree/part/admin.py
+++ b/InvenTree/part/admin.py
@@ -19,7 +19,7 @@ class PartCategoryAdmin(ImportExportModelAdmin):
class PartAttachmentAdmin(admin.ModelAdmin):
- list_display = ('part', 'attachment')
+ list_display = ('part', 'attachment', 'comment')
class BomItemAdmin(ImportExportModelAdmin):
diff --git a/InvenTree/part/migrations/0013_auto_20190429_2229.py b/InvenTree/part/migrations/0013_auto_20190429_2229.py
new file mode 100644
index 0000000000..9c339b18b1
--- /dev/null
+++ b/InvenTree/part/migrations/0013_auto_20190429_2229.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2 on 2019-04-29 12:29
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('part', '0012_part_active'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='part',
+ name='URL',
+ field=models.URLField(blank=True, help_text='Link to external URL'),
+ ),
+ ]
diff --git a/InvenTree/part/migrations/0014_partattachment_comment.py b/InvenTree/part/migrations/0014_partattachment_comment.py
new file mode 100644
index 0000000000..a51f588a59
--- /dev/null
+++ b/InvenTree/part/migrations/0014_partattachment_comment.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2 on 2019-04-30 23:34
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('part', '0013_auto_20190429_2229'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='partattachment',
+ name='comment',
+ field=models.CharField(blank=True, help_text='Attachment description', max_length=100),
+ ),
+ ]
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index 04d5471f92..19682929cf 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -125,7 +125,7 @@ class Part(models.Model):
IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number')
# Provide a URL for an external link
- URL = models.URLField(blank=True, help_text='Link to extenal URL')
+ URL = models.URLField(blank=True, help_text='Link to external URL')
# Part category - all parts must be assigned to a category
category = models.ForeignKey(PartCategory, related_name='parts',
@@ -307,6 +307,12 @@ class Part(models.Model):
def used_in_count(self):
return self.used_in.count()
+ def required_parts(self):
+ parts = []
+ for bom in self.bom_items.all():
+ parts.append(bom.sub_part)
+ return parts
+
@property
def supplier_count(self):
# Return the number of supplier parts available for this part
@@ -360,6 +366,8 @@ class PartAttachment(models.Model):
attachment = models.FileField(upload_to=attach_file, null=True, blank=True)
+ comment = models.CharField(max_length=100, blank=True, help_text="Attachment description")
+
@property
def basename(self):
return os.path.basename(self.attachment.name)
@@ -399,8 +407,11 @@ class BomItem(models.Model):
- A part cannot refer to a part which refers to it
"""
+ if self.part is None or self.sub_part is None:
+ # Field validation will catch these None values
+ pass
# A part cannot refer to itself in its BOM
- if self.part == self.sub_part:
+ elif self.part == self.sub_part:
raise ValidationError({'sub_part': _('Part cannot be added to its own Bill of Materials')})
# Test for simple recursion
diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html
index 805f501dcd..c178ef455c 100644
--- a/InvenTree/part/templates/part/bom.html
+++ b/InvenTree/part/templates/part/bom.html
@@ -60,7 +60,14 @@
});
$("#bom-item-new").click(function () {
- launchModalForm("{% url 'bom-item-create' %}?parent={{ part.id }}", {});
+ launchModalForm(
+ "{% url 'bom-item-create' %}?parent={{ part.id }}",
+ {
+ success: function() {
+ $("#bom-table").bootstrapTable('refresh');
+ }
+ }
+ );
});
{% else %}
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index 1d92ee651a..7ea8e5efbf 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -73,7 +73,6 @@ class PartCreate(AjaxCreateView):
def get_category_id(self):
return self.request.GET.get('category', None)
- # If a category is provided in the URL, pass that to the page context
def get_context_data(self, **kwargs):
""" Provide extra context information for the form to display:
@@ -99,7 +98,7 @@ class PartCreate(AjaxCreateView):
form = super(AjaxCreateView, self).get_form()
# Hide the default_supplier field (there are no matching supplier parts yet!)
- form.fields['default_supplier'] = HiddenInput()
+ form.fields['default_supplier'].widget = HiddenInput()
return form
@@ -385,6 +384,35 @@ class BomItemCreate(AjaxCreateView):
ajax_template_name = 'modal_form.html'
ajax_form_title = 'Create BOM item'
+ def get_form(self):
+ """ Override get_form() method to reduce Part selection options.
+
+ - Do not allow part to be added to its own BOM
+ - Remove any Part items that are already in the BOM
+ """
+
+ form = super(AjaxCreateView, self).get_form()
+
+ part_id = form['part'].value()
+
+ try:
+ part = Part.objects.get(id=part_id)
+
+ # Don't allow selection of sub_part objects which are already added to the Bom!
+ query = form.fields['sub_part'].queryset
+
+ # Don't allow a part to be added to its own BOM
+ query = query.exclude(id=part.id)
+
+ # Eliminate any options that are already in the BOM!
+ query = query.exclude(id__in=[item.id for item in part.required_parts()])
+
+ form.fields['sub_part'].queryset = query
+ except Part.DoesNotExist:
+ pass
+
+ return form
+
def get_initial(self):
""" Provide initial data for the BomItem:
diff --git a/InvenTree/static/css/inventree.css b/InvenTree/static/css/inventree.css
index 14b021adc7..0a1c34036b 100644
--- a/InvenTree/static/css/inventree.css
+++ b/InvenTree/static/css/inventree.css
@@ -122,6 +122,10 @@
margin-right: 2px;
}
+.panel-group {
+ margin-bottom: 5px;
+}
+
.float-right {
float: right;
}
@@ -135,4 +139,16 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
-}
\ No newline at end of file
+}
+
+.part-allocation-pass {
+ background: #dbf0db;
+}
+
+.part-allocation-underallocated {
+ background: #f0dbdb;
+}
+
+.part-allocation-overallocated {
+ background: #ccf5ff;
+}
diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js
index cc533f5201..1b58b81a93 100644
--- a/InvenTree/static/script/inventree/bom.js
+++ b/InvenTree/static/script/inventree/bom.js
@@ -114,6 +114,41 @@ function loadBomTable(table, options) {
}
);
+ if (options.editable) {
+ cols.push({
+ formatter: function(value, row, index, field) {
+ var bEdit = "Edit ";
+ var bDelt = "Delete ";
+
+ return "" + bEdit + bDelt + "
";
+ }
+ });
+ }
+ else {
+ cols.push(
+ {
+ field: 'sub_part_detail.available_stock',
+ title: 'Available',
+ searchable: false,
+ sortable: true,
+ formatter: function(value, row, index, field) {
+ var text = "";
+
+ if (row.quantity < row.sub_part_detail.available_stock)
+ {
+ text = "" + value + " ";
+ }
+ else
+ {
+ text = "" + value + " ";
+ }
+
+ return renderLink(text, row.sub_part.url + "stock/");
+ }
+ }
+ );
+ }
+
// Part notes
cols.push(
{
@@ -124,43 +159,8 @@ function loadBomTable(table, options) {
}
);
- if (options.editable) {
- cols.push({
- formatter: function(value, row, index, field) {
- var bEdit = "Edit ";
- var bDelt = "Delete ";
-
- return "" + bEdit + bDelt + "
";
- }
- });
- }
- else {
- cols.push(
- {
- field: 'sub_part_detail.available_stock',
- title: 'Available',
- searchable: false,
- sortable: true,
- formatter: function(value, row, index, field) {
- var text = "";
-
- if (row.quantity < row.sub_part_detail.available_stock)
- {
- text = "" + value + " ";
- }
- else
- {
- text = "" + value + " ";
- }
-
- return renderLink(text, row.sub_part.url + "stock/");
- }
- }
- );
- }
-
// Configure the table (bootstrap-table)
-
+
table.bootstrapTable({
sortable: true,
search: true,
@@ -168,11 +168,11 @@ function loadBomTable(table, options) {
queryParams: function(p) {
return {
part: options.parent_id,
- }
- },
- columns: cols,
- url: options.bom_url
- });
+ }
+ },
+ columns: cols,
+ url: options.bom_url
+});
// In editing mode, attached editables to the appropriate table elements
if (options.editable) {
diff --git a/InvenTree/static/script/inventree/build.js b/InvenTree/static/script/inventree/build.js
new file mode 100644
index 0000000000..826e2b1661
--- /dev/null
+++ b/InvenTree/static/script/inventree/build.js
@@ -0,0 +1,97 @@
+function updateAllocationTotal(id, count, required) {
+
+
+ $('#allocation-total-'+id).html(count);
+
+ var el = $("#allocation-panel-" + id);
+ el.removeClass('part-allocation-pass part-allocation-underallocated part-allocation-overallocated');
+
+ if (count < required) {
+ el.addClass('part-allocation-underallocated');
+ } else if (count > required) {
+ el.addClass('part-allocation-overallocated');
+ } else {
+ el.addClass('part-allocation-pass');
+ }
+}
+
+function loadAllocationTable(table, part_id, part, url, required, button) {
+
+ // Load the allocation table
+ table.bootstrapTable({
+ url: url,
+ sortable: false,
+ formatNoMatches: function() { return 'No parts allocated for ' + part; },
+ columns: [
+ {
+ field: 'stock_item_detail',
+ title: 'Stock Item',
+ formatter: function(value, row, index, field) {
+ return '' + value.quantity + ' x ' + value.part_name + ' @ ' + value.location_name;
+ }
+ },
+ {
+ field: 'stock_item_detail.quantity',
+ title: 'Available',
+ },
+ {
+ field: 'quantity',
+ title: 'Allocated',
+ formatter: function(value, row, index, field) {
+ var html = value;
+
+ var bEdit = "Edit ";
+ var bDel = "Delete ";
+
+ html += "" + bEdit + bDel + "
";
+
+ return html;
+ }
+ }
+ ],
+ });
+
+ // Callback for 'new-item' button
+ button.click(function() {
+ launchModalForm(button.attr('url'), {
+ success: function() {
+ table.bootstrapTable('refresh');
+ }
+ });
+ });
+
+ table.on('load-success.bs.table', function(data) {
+ // Extract table data
+ var results = table.bootstrapTable('getData');
+
+ var count = 0;
+
+ for (var i = 0; i < results.length; i++) {
+ count += results[i].quantity;
+ }
+
+ updateAllocationTotal(part_id, count, required);
+ });
+
+ // Button callbacks for editing and deleting the allocations
+ table.on('click', '.item-edit-button', function() {
+ var button = $(this);
+
+ launchModalForm(button.attr('url'), {
+ success: function() {
+ table.bootstrapTable('refresh');
+ }
+ });
+ });
+
+ table.on('click', '.item-del-button', function() {
+ var button = $(this);
+
+ launchDeleteForm(button.attr('url'), {
+ success: function() {
+ table.bootstrapTable('refresh');
+ }
+ });
+ });
+
+}
\ No newline at end of file
diff --git a/InvenTree/static/script/inventree/part.js b/InvenTree/static/script/inventree/part.js
index 9396e01772..053355113b 100644
--- a/InvenTree/static/script/inventree/part.js
+++ b/InvenTree/static/script/inventree/part.js
@@ -15,5 +15,5 @@ function getPartList(filters={}, options={}) {
}
function getBomList(filters={}, options={}) {
- return inventreeGet('/api/part/bom/', filters, options);
+ return inventreeGet('/api/bom/', filters, options);
}
\ No newline at end of file
diff --git a/InvenTree/static/script/inventree/tables.js b/InvenTree/static/script/inventree/tables.js
index 1ff1903326..979a7d5a21 100644
--- a/InvenTree/static/script/inventree/tables.js
+++ b/InvenTree/static/script/inventree/tables.js
@@ -2,10 +2,12 @@ function editButton(url, text='Edit') {
return "" + text + " ";
}
+
function deleteButton(url, text='Delete') {
return "" + text + " ";
}
+
function renderLink(text, url) {
if (text === '' || url === '') {
return text;
@@ -14,51 +16,6 @@ function renderLink(text, url) {
return '' + text + ' ';
}
-function renderEditable(text, options) {
- /* Wrap the text in an 'editable' link
- * (using bootstrap-editable library)
- *
- * Can pass the following parameters in 'options':
- * _type - parameter for data-type (default = 'text')
- * _pk - parameter for data-pk (required)
- * _title - title to show when editing
- * _empty - placeholder text to show when field is empty
- * _class - html class (default = 'editable-item')
- * _id - id
- * _value - Initial value of the editable (default = blank)
- */
-
- // Default values (if not supplied)
- var _type = options._type || 'text';
- var _class = options._class || 'editable-item';
-
- var html = "" + text + " ";
-
- return html;
-}
function enableButtons(elements, enabled) {
for (let item of elements) {
diff --git a/InvenTree/stock/migrations/0010_auto_20190501_2344.py b/InvenTree/stock/migrations/0010_auto_20190501_2344.py
new file mode 100644
index 0000000000..61ea730b03
--- /dev/null
+++ b/InvenTree/stock/migrations/0010_auto_20190501_2344.py
@@ -0,0 +1,18 @@
+# Generated by Django 2.2 on 2019-05-01 13:44
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stock', '0009_auto_20190428_0841'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='stockitem',
+ name='batch',
+ field=models.CharField(blank=True, help_text='Batch code for this stock item', max_length=100, null=True),
+ ),
+ ]
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index 2f61c068cd..cb0ad8aea4 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -158,7 +158,7 @@ class StockItem(models.Model):
URL = models.URLField(max_length=125, blank=True)
# Optional batch information
- batch = models.CharField(max_length=100, blank=True,
+ batch = models.CharField(max_length=100, blank=True, null=True,
help_text='Batch code for this stock item')
# If this part was produced by a build, point to that build here
diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py
index 0a863aa7f9..269e3b4e78 100644
--- a/InvenTree/stock/serializers.py
+++ b/InvenTree/stock/serializers.py
@@ -29,12 +29,9 @@ class LocationBriefSerializer(serializers.ModelSerializer):
class StockItemSerializerBrief(serializers.ModelSerializer):
- """
- Provide a brief serializer for StockItem
- """
-
- url = serializers.CharField(source='get_absolute_url', read_only=True)
+ """ Brief serializers for a StockItem """
+ location_name = serializers.CharField(source='location', read_only=True)
part_name = serializers.CharField(source='part.name', read_only=True)
class Meta:
@@ -42,8 +39,12 @@ class StockItemSerializerBrief(serializers.ModelSerializer):
fields = [
'pk',
'uuid',
- 'url',
+ 'part',
'part_name',
+ 'supplier_part',
+ 'location',
+ 'location_name',
+ 'quantity',
]
diff --git a/InvenTree/templates/modal_form.html b/InvenTree/templates/modal_form.html
index 1318e4f238..566671c657 100644
--- a/InvenTree/templates/modal_form.html
+++ b/InvenTree/templates/modal_form.html
@@ -1,3 +1,6 @@
+{% block pre_form_content %}
+{% endblock %}
+
{% if form.non_field_errors %}
Error Submitting Form:
@@ -11,4 +14,7 @@
{% crispy form %}
-
\ No newline at end of file
+
+
+{% block post_form_content %}
+{% endblock %}
\ No newline at end of file
diff --git a/InvenTree/templates/modals.html b/InvenTree/templates/modals.html
index a84e4c7b44..9ba0335572 100644
--- a/InvenTree/templates/modals.html
+++ b/InvenTree/templates/modals.html
@@ -32,7 +32,7 @@