mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #189 from SchrodingersGat/build-allocation
Build allocation
This commit is contained in:
commit
dde8657612
InvenTree
InvenTree
build
admin.pyapi.pyforms.py
migrations
0003_builditemallocation.py0004_build_url.py0005_auto_20190429_2229.py0006_auto_20190429_2233.py0007_auto_20190429_2255.py0008_auto_20190501_2344.py
models.pyserializers.pytemplates/build
templatetags
urls.pyviews.pycompany/templates/company
part
static
stock
templates
@ -199,7 +199,7 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
|
|||||||
|
|
||||||
form = self.get_form()
|
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):
|
def post(self, request, *args, **kwargs):
|
||||||
""" Respond to POST request.
|
""" Respond to POST request.
|
||||||
|
@ -2,21 +2,33 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.contrib import admin
|
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',
|
list_display = (
|
||||||
'status',
|
'part',
|
||||||
'batch',
|
'status',
|
||||||
'quantity',
|
'batch',
|
||||||
'creation_date',
|
'quantity',
|
||||||
'completion_date',
|
'creation_date',
|
||||||
'title',
|
'completion_date',
|
||||||
'notes',
|
'title',
|
||||||
)
|
'notes',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BuildItemAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
list_display = (
|
||||||
|
'build',
|
||||||
|
'stock_item',
|
||||||
|
'quantity'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Build, BuildAdmin)
|
admin.site.register(Build, BuildAdmin)
|
||||||
|
admin.site.register(BuildItem, BuildItemAdmin)
|
||||||
|
@ -9,10 +9,10 @@ from django_filters.rest_framework import DjangoFilterBackend
|
|||||||
from rest_framework import filters
|
from rest_framework import filters
|
||||||
from rest_framework import generics, permissions
|
from rest_framework import generics, permissions
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url, include
|
||||||
|
|
||||||
from .models import Build
|
from .models import Build, BuildItem
|
||||||
from .serializers import BuildSerializer
|
from .serializers import BuildSerializer, BuildItemSerializer
|
||||||
|
|
||||||
|
|
||||||
class BuildList(generics.ListCreateAPIView):
|
class BuildList(generics.ListCreateAPIView):
|
||||||
@ -40,6 +40,50 @@ class BuildList(generics.ListCreateAPIView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
build_api_urls = [
|
class BuildItemList(generics.ListCreateAPIView):
|
||||||
url(r'^.*$', BuildList.as_view(), name='api-build-list')
|
""" 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'),
|
||||||
]
|
]
|
||||||
|
@ -6,8 +6,9 @@ Django Forms for interacting with Build objects
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from InvenTree.forms import HelperForm
|
from InvenTree.forms import HelperForm
|
||||||
|
from django import forms
|
||||||
from .models import Build
|
from .models import Build, BuildItem
|
||||||
|
from stock.models import StockLocation
|
||||||
|
|
||||||
|
|
||||||
class EditBuildForm(HelperForm):
|
class EditBuildForm(HelperForm):
|
||||||
@ -21,7 +22,36 @@ class EditBuildForm(HelperForm):
|
|||||||
'part',
|
'part',
|
||||||
'quantity',
|
'quantity',
|
||||||
'batch',
|
'batch',
|
||||||
|
'URL',
|
||||||
'notes',
|
'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',
|
||||||
]
|
]
|
||||||
|
25
InvenTree/build/migrations/0003_builditemallocation.py
Normal file
25
InvenTree/build/migrations/0003_builditemallocation.py
Normal file
@ -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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
18
InvenTree/build/migrations/0004_build_url.py
Normal file
18
InvenTree/build/migrations/0004_build_url.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
18
InvenTree/build/migrations/0005_auto_20190429_2229.py
Normal file
18
InvenTree/build/migrations/0005_auto_20190429_2229.py
Normal file
@ -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',
|
||||||
|
),
|
||||||
|
]
|
18
InvenTree/build/migrations/0006_auto_20190429_2233.py
Normal file
18
InvenTree/build/migrations/0006_auto_20190429_2233.py
Normal file
@ -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',
|
||||||
|
),
|
||||||
|
]
|
18
InvenTree/build/migrations/0007_auto_20190429_2255.py
Normal file
18
InvenTree/build/migrations/0007_auto_20190429_2255.py
Normal file
@ -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')},
|
||||||
|
),
|
||||||
|
]
|
25
InvenTree/build/migrations/0008_auto_20190501_2344.py
Normal file
25
InvenTree/build/migrations/0008_auto_20190501_2344.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
@ -5,17 +5,79 @@ Build database model definitions
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.db import models
|
from django.db import models, transaction
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
|
|
||||||
|
from stock.models import StockItem
|
||||||
|
|
||||||
|
|
||||||
class Build(models.Model):
|
class Build(models.Model):
|
||||||
""" A Build object organises the creation of new parts from the component parts.
|
""" 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):
|
def get_absolute_url(self):
|
||||||
return reverse('build-detail', kwargs={'pk': self.id})
|
return reverse('build-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
@ -23,46 +85,101 @@ class Build(models.Model):
|
|||||||
related_name='builds',
|
related_name='builds',
|
||||||
limit_choices_to={'buildable': True},
|
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')
|
title = models.CharField(max_length=100, help_text='Brief description of the build')
|
||||||
|
|
||||||
#: Number of output parts to build
|
quantity = models.PositiveIntegerField(
|
||||||
quantity = models.PositiveIntegerField(default=1,
|
default=1,
|
||||||
validators=[MinValueValidator(1)],
|
validators=[MinValueValidator(1)],
|
||||||
help_text='Number of parts to build')
|
help_text='Number of parts to build'
|
||||||
|
)
|
||||||
|
|
||||||
# Build status codes
|
# Build status codes
|
||||||
PENDING = 10 # Build is pending / active
|
PENDING = 10 # Build is pending / active
|
||||||
HOLDING = 20 # Build is currently being held
|
|
||||||
CANCELLED = 30 # Build was cancelled
|
CANCELLED = 30 # Build was cancelled
|
||||||
COMPLETE = 40 # Build is complete
|
COMPLETE = 40 # Build is complete
|
||||||
|
|
||||||
#: Build status codes
|
#: Build status codes
|
||||||
BUILD_STATUS_CODES = {PENDING: _("Pending"),
|
BUILD_STATUS_CODES = {PENDING: _("Pending"),
|
||||||
HOLDING: _("Holding"),
|
|
||||||
CANCELLED: _("Cancelled"),
|
CANCELLED: _("Cancelled"),
|
||||||
COMPLETE: _("Complete"),
|
COMPLETE: _("Complete"),
|
||||||
}
|
}
|
||||||
|
|
||||||
#: Status of the build (ref BUILD_STATUS_CODES)
|
|
||||||
status = models.PositiveIntegerField(default=PENDING,
|
status = models.PositiveIntegerField(default=PENDING,
|
||||||
choices=BUILD_STATUS_CODES.items(),
|
choices=BUILD_STATUS_CODES.items(),
|
||||||
validators=[MinValueValidator(0)])
|
validators=[MinValueValidator(0)])
|
||||||
|
|
||||||
#: Batch number for the build (optional)
|
|
||||||
batch = models.CharField(max_length=100, blank=True, null=True,
|
batch = models.CharField(max_length=100, blank=True, null=True,
|
||||||
help_text='Batch code for this build output')
|
help_text='Batch code for this build output')
|
||||||
|
|
||||||
#: Date the build model was 'created'
|
|
||||||
creation_date = models.DateField(auto_now=True, editable=False)
|
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)
|
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 = 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
|
@property
|
||||||
def required_parts(self):
|
def required_parts(self):
|
||||||
@ -99,10 +216,77 @@ class Build(models.Model):
|
|||||||
|
|
||||||
return self.status in [
|
return self.status in [
|
||||||
self.PENDING,
|
self.PENDING,
|
||||||
self.HOLDING
|
|
||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_complete(self):
|
def is_complete(self):
|
||||||
""" Returns True if the build status is COMPLETE """
|
""" Returns True if the build status is COMPLETE """
|
||||||
return self.status == self.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'
|
||||||
|
)
|
||||||
|
@ -6,11 +6,13 @@ JSON serializers for Build API
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from rest_framework import serializers
|
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 """
|
""" Serializes a Build object """
|
||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
||||||
@ -29,3 +31,30 @@ class BuildSerializer(serializers.ModelSerializer):
|
|||||||
'status',
|
'status',
|
||||||
'status_text',
|
'status_text',
|
||||||
'notes']
|
'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'
|
||||||
|
]
|
||||||
|
@ -1,47 +1,59 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h3>Allocate Parts for Build</h3>
|
<h3>Allocate Parts for Build</h3>
|
||||||
|
|
||||||
<h4><a href="{% url 'build-detail' build.id %}">{{ build.title }}</a></h4>
|
<h4><a href="{% url 'build-detail' build.id %}">{{ build.title }}</a></h4>
|
||||||
|
{{ build.quantity }} x {{ build.part.name }}
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
{% for bom_item in bom_items.all %}
|
||||||
|
{% include "build/allocation_item.html" with item=bom_item build=build %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
<table class='table table-striped' id='build-table'>
|
<table class='table table-striped' id='build-table'>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class='btn btn-warning' type='button' id='complete-build'>Complete Build</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_load %}
|
{% block js_load %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<script src="{% static 'script/inventree/api.js' %}"></script>
|
<script src="{% static 'script/inventree/api.js' %}"></script>
|
||||||
<script src="{% static 'script/inventree/part.js' %}"></script>
|
<script src="{% static 'script/inventree/part.js' %}"></script>
|
||||||
|
<script src="{% static 'script/inventree/build.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
$('#build-table').bootstrapTable({
|
{% for bom_item in bom_items.all %}
|
||||||
sortable: true,
|
|
||||||
columns: [
|
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',
|
reload: true,
|
||||||
title: 'Part',
|
submit_text: "Complete Build",
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Source',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'quantity',
|
|
||||||
title: 'Quantity',
|
|
||||||
}
|
}
|
||||||
],
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
getBomList({part: {{ build.part.id }}}).then(function(response) {
|
{% endblock %}
|
||||||
$("#build-table").bootstrapTable('load', response);
|
|
||||||
});
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
38
InvenTree/build/templates/build/allocation_item.html
Normal file
38
InvenTree/build/templates/build/allocation_item.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
|
<div class='panel-group'>
|
||||||
|
<div class='panel pane-default'>
|
||||||
|
<div class='panel panel-heading'>
|
||||||
|
<div class='row'>
|
||||||
|
<div class='col-sm-6'>
|
||||||
|
<div class='panel-title'>
|
||||||
|
<a data-toggle='collapse' href='#collapse-item-{{ item.id }}'>{{ item.sub_part.name }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='col-sm-1' align='right'>
|
||||||
|
Required:
|
||||||
|
</div>
|
||||||
|
<div class='col-sm-1'>
|
||||||
|
<b>{% multiply build.quantity item.quantity %}</b>
|
||||||
|
</div>
|
||||||
|
<div class='col-sm-1' align='right'>
|
||||||
|
Allocated:
|
||||||
|
</div>
|
||||||
|
<div class='col-sm-1' id='allocation-panel-{{ item.sub_part.id }}'>
|
||||||
|
<b><span id='allocation-total-{{ item.sub_part.id }}'>0</span></b>
|
||||||
|
</div>
|
||||||
|
<div class='col-sm-2'>
|
||||||
|
<div class='btn-group' style='float: right;'>
|
||||||
|
<button class='btn btn-success btn-sm' id='new-item-{{ item.sub_part.id }}' url="{% url 'build-item-create' %}?part={{ item.sub_part.id }}&build={{ build.id }}">Allocate Parts</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id='collapse-item-{{ item.id }}' class='panel-collapse collapse'>
|
||||||
|
<div class='panel-body'>
|
||||||
|
<table class='table table-striped table-condensed' id='allocate-table-id-{{ item.sub_part.id }}'>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
24
InvenTree/build/templates/build/complete.html
Normal file
24
InvenTree/build/templates/build/complete.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{% extends "modal_form.html" %}
|
||||||
|
|
||||||
|
{% block pre_form_content %}
|
||||||
|
<b>Build: {{ build.title }}</b> - {{ build.quantity }} x {{ build.part.name }}
|
||||||
|
<br>
|
||||||
|
Are you sure you want to mark this build as complete?
|
||||||
|
<hr>
|
||||||
|
{% if taking %}
|
||||||
|
The following items will be removed from stock:
|
||||||
|
<ul>
|
||||||
|
{% for item in taking %}
|
||||||
|
<li>{{ item.quantity }} x {{ item.stock_item.part.name }} from {{ item.stock_item.location }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
No parts have been allocated to this build.
|
||||||
|
{% endif %}
|
||||||
|
<hr>
|
||||||
|
The following items will be created:
|
||||||
|
<ul>
|
||||||
|
<li>{{ build.quantity }} x {{ build.part.name }}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% endblock %}
|
3
InvenTree/build/templates/build/delete_build_item.html
Normal file
3
InvenTree/build/templates/build/delete_build_item.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Are you sure you want to unallocate these parts?
|
||||||
|
<br>
|
||||||
|
This will remove {{ item.quantity }} parts from build '{{ item.build.title }}'.
|
@ -38,14 +38,19 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Quantity</td><td>{{ build.quantity }}</td>
|
<td>Quantity</td><td>{{ build.quantity }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Status</td><td>{% include "build_status.html" with build=build %}</td>
|
||||||
|
</tr>
|
||||||
{% if build.batch %}
|
{% if build.batch %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Batch</td><td>{{ build.batch }}</td>
|
<td>Batch</td><td>{{ build.batch }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if build.URL %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Status</td><td>{% include "build_status.html" with build=build %}</td>
|
<td>URL</td><td><a href="{{ build.URL }}">{{ build.URL }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Created</td><td>{{ build.creation_date }}</td>
|
<td>Created</td><td>{{ build.creation_date }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -73,6 +78,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{% if build.is_active %}
|
||||||
<h3>Required Parts</h3>
|
<h3>Required Parts</h3>
|
||||||
<table class='table table-striped' id='build-list' data-sorting='true'>
|
<table class='table table-striped' id='build-list' data-sorting='true'>
|
||||||
<thead>
|
<thead>
|
||||||
@ -93,6 +99,8 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% include 'modals.html' %}
|
{% include 'modals.html' %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -110,7 +118,7 @@
|
|||||||
launchModalForm("{% url 'build-cancel' build.id %}",
|
launchModalForm("{% url 'build-cancel' build.id %}",
|
||||||
{
|
{
|
||||||
reload: true,
|
reload: true,
|
||||||
submit_text: "Cancel",
|
submit_text: "Cancel Build",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
0
InvenTree/build/templatetags/__init__.py
Normal file
0
InvenTree/build/templatetags/__init__.py
Normal file
12
InvenTree/build/templatetags/inventree_extras.py
Normal file
12
InvenTree/build/templatetags/inventree_extras.py
Normal file
@ -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
|
@ -6,16 +6,29 @@ from django.conf.urls import url, include
|
|||||||
|
|
||||||
from . import views
|
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<pk>\d+)/', include(build_item_detail_urls)),
|
||||||
|
url('^new/', views.BuildItemCreate.as_view(), name='build-item-create'),
|
||||||
|
]
|
||||||
|
|
||||||
build_detail_urls = [
|
build_detail_urls = [
|
||||||
url(r'^edit/?', views.BuildUpdate.as_view(), name='build-edit'),
|
url(r'^edit/?', views.BuildUpdate.as_view(), name='build-edit'),
|
||||||
url(r'^allocate/?', views.BuildAllocate.as_view(), name='build-allocate'),
|
url(r'^allocate/?', views.BuildAllocate.as_view(), name='build-allocate'),
|
||||||
url(r'^cancel/?', views.BuildCancel.as_view(), name='build-cancel'),
|
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'),
|
url(r'^.*$', views.BuildDetail.as_view(), name='build-detail'),
|
||||||
]
|
]
|
||||||
|
|
||||||
build_urls = [
|
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<pk>\d+)/', include(build_detail_urls)),
|
url(r'^(?P<pk>\d+)/', include(build_detail_urls)),
|
||||||
|
|
||||||
|
@ -8,12 +8,14 @@ from __future__ import unicode_literals
|
|||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from django.views.generic import DetailView, ListView
|
from django.views.generic import DetailView, ListView
|
||||||
|
from django.forms import HiddenInput
|
||||||
|
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from .models import Build
|
from .models import Build, BuildItem
|
||||||
from .forms import EditBuildForm
|
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):
|
class BuildIndex(ListView):
|
||||||
@ -46,7 +48,7 @@ class BuildCancel(AjaxView):
|
|||||||
Provides a cancellation information dialog
|
Provides a cancellation information dialog
|
||||||
"""
|
"""
|
||||||
model = Build
|
model = Build
|
||||||
template_name = 'build/cancel.html'
|
ajax_template_name = 'build/cancel.html'
|
||||||
ajax_form_title = 'Cancel Build'
|
ajax_form_title = 'Cancel Build'
|
||||||
context_object_name = 'build'
|
context_object_name = 'build'
|
||||||
fields = []
|
fields = []
|
||||||
@ -56,15 +58,109 @@ class BuildCancel(AjaxView):
|
|||||||
|
|
||||||
build = get_object_or_404(Build, pk=self.kwargs['pk'])
|
build = get_object_or_404(Build, pk=self.kwargs['pk'])
|
||||||
|
|
||||||
build.status = Build.CANCELLED
|
build.cancelBuild()
|
||||||
build.save()
|
|
||||||
|
|
||||||
return self.renderJsonResponse(request, None)
|
return self.renderJsonResponse(request, None)
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
""" Provide JSON context data. """
|
""" Provide JSON context data. """
|
||||||
return {
|
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'
|
context_object_name = 'build'
|
||||||
template_name = 'build/allocate.html'
|
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):
|
class BuildCreate(AjaxCreateView):
|
||||||
""" View to create a new Build object """
|
""" View to create a new Build object """
|
||||||
@ -127,3 +237,116 @@ class BuildUpdate(AjaxUpdateView):
|
|||||||
return {
|
return {
|
||||||
'info': 'Edited build',
|
'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
|
||||||
|
@ -19,4 +19,4 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -19,7 +19,7 @@ class PartCategoryAdmin(ImportExportModelAdmin):
|
|||||||
|
|
||||||
class PartAttachmentAdmin(admin.ModelAdmin):
|
class PartAttachmentAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
list_display = ('part', 'attachment')
|
list_display = ('part', 'attachment', 'comment')
|
||||||
|
|
||||||
|
|
||||||
class BomItemAdmin(ImportExportModelAdmin):
|
class BomItemAdmin(ImportExportModelAdmin):
|
||||||
|
18
InvenTree/part/migrations/0013_auto_20190429_2229.py
Normal file
18
InvenTree/part/migrations/0013_auto_20190429_2229.py
Normal file
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
18
InvenTree/part/migrations/0014_partattachment_comment.py
Normal file
18
InvenTree/part/migrations/0014_partattachment_comment.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -125,7 +125,7 @@ class Part(models.Model):
|
|||||||
IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number')
|
IPN = models.CharField(max_length=100, blank=True, help_text='Internal Part Number')
|
||||||
|
|
||||||
# Provide a URL for an external link
|
# 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
|
# Part category - all parts must be assigned to a category
|
||||||
category = models.ForeignKey(PartCategory, related_name='parts',
|
category = models.ForeignKey(PartCategory, related_name='parts',
|
||||||
@ -307,6 +307,12 @@ class Part(models.Model):
|
|||||||
def used_in_count(self):
|
def used_in_count(self):
|
||||||
return self.used_in.count()
|
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
|
@property
|
||||||
def supplier_count(self):
|
def supplier_count(self):
|
||||||
# Return the number of supplier parts available for this part
|
# 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)
|
attachment = models.FileField(upload_to=attach_file, null=True, blank=True)
|
||||||
|
|
||||||
|
comment = models.CharField(max_length=100, blank=True, help_text="Attachment description")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def basename(self):
|
def basename(self):
|
||||||
return os.path.basename(self.attachment.name)
|
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
|
- 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
|
# 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')})
|
raise ValidationError({'sub_part': _('Part cannot be added to its own Bill of Materials')})
|
||||||
|
|
||||||
# Test for simple recursion
|
# Test for simple recursion
|
||||||
|
@ -60,7 +60,14 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#bom-item-new").click(function () {
|
$("#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 %}
|
{% else %}
|
||||||
|
@ -73,7 +73,6 @@ class PartCreate(AjaxCreateView):
|
|||||||
def get_category_id(self):
|
def get_category_id(self):
|
||||||
return self.request.GET.get('category', None)
|
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):
|
def get_context_data(self, **kwargs):
|
||||||
""" Provide extra context information for the form to display:
|
""" Provide extra context information for the form to display:
|
||||||
|
|
||||||
@ -99,7 +98,7 @@ class PartCreate(AjaxCreateView):
|
|||||||
form = super(AjaxCreateView, self).get_form()
|
form = super(AjaxCreateView, self).get_form()
|
||||||
|
|
||||||
# Hide the default_supplier field (there are no matching supplier parts yet!)
|
# 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
|
return form
|
||||||
|
|
||||||
@ -385,6 +384,35 @@ class BomItemCreate(AjaxCreateView):
|
|||||||
ajax_template_name = 'modal_form.html'
|
ajax_template_name = 'modal_form.html'
|
||||||
ajax_form_title = 'Create BOM item'
|
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):
|
def get_initial(self):
|
||||||
""" Provide initial data for the BomItem:
|
""" Provide initial data for the BomItem:
|
||||||
|
|
||||||
|
@ -122,6 +122,10 @@
|
|||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-group {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.float-right {
|
.float-right {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
@ -135,4 +139,16 @@
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.part-allocation-pass {
|
||||||
|
background: #dbf0db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.part-allocation-underallocated {
|
||||||
|
background: #f0dbdb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.part-allocation-overallocated {
|
||||||
|
background: #ccf5ff;
|
||||||
|
}
|
||||||
|
@ -114,6 +114,41 @@ function loadBomTable(table, options) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (options.editable) {
|
||||||
|
cols.push({
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
var bEdit = "<button class='btn btn-success bom-edit-button btn-sm' type='button' url='/part/bom/" + row.pk + "/edit'>Edit</button>";
|
||||||
|
var bDelt = "<button class='btn btn-danger bom-delete-button btn-sm' type='button' url='/part/bom/" + row.pk + "/delete'>Delete</button>";
|
||||||
|
|
||||||
|
return "<div class='btn-group'>" + bEdit + bDelt + "</div>";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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 = "<span class='label label-success'>" + value + "</span>";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
text = "<span class='label label-warning'>" + value + "</span>";
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderLink(text, row.sub_part.url + "stock/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Part notes
|
// Part notes
|
||||||
cols.push(
|
cols.push(
|
||||||
{
|
{
|
||||||
@ -124,43 +159,8 @@ function loadBomTable(table, options) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (options.editable) {
|
|
||||||
cols.push({
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
var bEdit = "<button class='btn btn-success bom-edit-button btn-sm' type='button' url='/part/bom/" + row.pk + "/edit'>Edit</button>";
|
|
||||||
var bDelt = "<button class='btn btn-danger bom-delete-button btn-sm' type='button' url='/part/bom/" + row.pk + "/delete'>Delete</button>";
|
|
||||||
|
|
||||||
return "<div class='btn-group'>" + bEdit + bDelt + "</div>";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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 = "<span class='label label-success'>" + value + "</span>";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
text = "<span class='label label-warning'>" + value + "</span>";
|
|
||||||
}
|
|
||||||
|
|
||||||
return renderLink(text, row.sub_part.url + "stock/");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure the table (bootstrap-table)
|
// Configure the table (bootstrap-table)
|
||||||
|
|
||||||
table.bootstrapTable({
|
table.bootstrapTable({
|
||||||
sortable: true,
|
sortable: true,
|
||||||
search: true,
|
search: true,
|
||||||
@ -168,11 +168,11 @@ function loadBomTable(table, options) {
|
|||||||
queryParams: function(p) {
|
queryParams: function(p) {
|
||||||
return {
|
return {
|
||||||
part: options.parent_id,
|
part: options.parent_id,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
columns: cols,
|
columns: cols,
|
||||||
url: options.bom_url
|
url: options.bom_url
|
||||||
});
|
});
|
||||||
|
|
||||||
// In editing mode, attached editables to the appropriate table elements
|
// In editing mode, attached editables to the appropriate table elements
|
||||||
if (options.editable) {
|
if (options.editable) {
|
||||||
|
97
InvenTree/static/script/inventree/build.js
Normal file
97
InvenTree/static/script/inventree/build.js
Normal file
@ -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 = "<button class='btn btn-success item-edit-button btn-sm' type='button' url='/build/item/" + row.pk + "/edit/'>Edit</button>";
|
||||||
|
var bDel = "<button class='btn btn-danger item-del-button btn-sm' type='button' url='/build/item/" + row.pk + "/delete/'>Delete</button>";
|
||||||
|
|
||||||
|
html += "<div class='btn-group' style='float: right;'>" + bEdit + bDel + "</div>";
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
@ -15,5 +15,5 @@ function getPartList(filters={}, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getBomList(filters={}, options={}) {
|
function getBomList(filters={}, options={}) {
|
||||||
return inventreeGet('/api/part/bom/', filters, options);
|
return inventreeGet('/api/bom/', filters, options);
|
||||||
}
|
}
|
@ -2,10 +2,12 @@ function editButton(url, text='Edit') {
|
|||||||
return "<button class='btn btn-success edit-button btn-sm' type='button' url='" + url + "'>" + text + "</button>";
|
return "<button class='btn btn-success edit-button btn-sm' type='button' url='" + url + "'>" + text + "</button>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function deleteButton(url, text='Delete') {
|
function deleteButton(url, text='Delete') {
|
||||||
return "<button class='btn btn-danger delete-button btn-sm' type='button' url='" + url + "'>" + text + "</button>";
|
return "<button class='btn btn-danger delete-button btn-sm' type='button' url='" + url + "'>" + text + "</button>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderLink(text, url) {
|
function renderLink(text, url) {
|
||||||
if (text === '' || url === '') {
|
if (text === '' || url === '') {
|
||||||
return text;
|
return text;
|
||||||
@ -14,51 +16,6 @@ function renderLink(text, url) {
|
|||||||
return '<a href="' + url + '">' + text + '</a>';
|
return '<a href="' + url + '">' + text + '</a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = "<a href='#' class='" + _class + "'";
|
|
||||||
|
|
||||||
// Add id parameter if provided
|
|
||||||
if (options._id) {
|
|
||||||
html = html + " id='" + options._id + "'";
|
|
||||||
}
|
|
||||||
|
|
||||||
html = html + " data-type='" + _type + "'";
|
|
||||||
html = html + " data-pk='" + options._pk + "'";
|
|
||||||
|
|
||||||
if (options._title) {
|
|
||||||
html = html + " data-title='" + options._title + "'";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options._value) {
|
|
||||||
html = html + " data-value='" + options._value + "'";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options._empty) {
|
|
||||||
html = html + " data-placeholder='" + options._empty + "'";
|
|
||||||
html = html + " data-emptytext='" + options._empty + "'";
|
|
||||||
}
|
|
||||||
|
|
||||||
html = html + ">" + text + "</a>";
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableButtons(elements, enabled) {
|
function enableButtons(elements, enabled) {
|
||||||
for (let item of elements) {
|
for (let item of elements) {
|
||||||
|
18
InvenTree/stock/migrations/0010_auto_20190501_2344.py
Normal file
18
InvenTree/stock/migrations/0010_auto_20190501_2344.py
Normal file
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -158,7 +158,7 @@ class StockItem(models.Model):
|
|||||||
URL = models.URLField(max_length=125, blank=True)
|
URL = models.URLField(max_length=125, blank=True)
|
||||||
|
|
||||||
# Optional batch information
|
# 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')
|
help_text='Batch code for this stock item')
|
||||||
|
|
||||||
# If this part was produced by a build, point to that build here
|
# If this part was produced by a build, point to that build here
|
||||||
|
@ -29,12 +29,9 @@ class LocationBriefSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class StockItemSerializerBrief(serializers.ModelSerializer):
|
class StockItemSerializerBrief(serializers.ModelSerializer):
|
||||||
"""
|
""" Brief serializers for a StockItem """
|
||||||
Provide a brief serializer for StockItem
|
|
||||||
"""
|
|
||||||
|
|
||||||
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
|
||||||
|
|
||||||
|
location_name = serializers.CharField(source='location', read_only=True)
|
||||||
part_name = serializers.CharField(source='part.name', read_only=True)
|
part_name = serializers.CharField(source='part.name', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -42,8 +39,12 @@ class StockItemSerializerBrief(serializers.ModelSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
'uuid',
|
'uuid',
|
||||||
'url',
|
'part',
|
||||||
'part_name',
|
'part_name',
|
||||||
|
'supplier_part',
|
||||||
|
'location',
|
||||||
|
'location_name',
|
||||||
|
'quantity',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
{% block pre_form_content %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% if form.non_field_errors %}
|
{% if form.non_field_errors %}
|
||||||
<div class='alert alert-danger' role='alert' style='display: block;'>
|
<div class='alert alert-danger' role='alert' style='display: block;'>
|
||||||
<b>Error Submitting Form:</b>
|
<b>Error Submitting Form:</b>
|
||||||
@ -11,4 +14,7 @@
|
|||||||
|
|
||||||
|
|
||||||
{% crispy form %}
|
{% crispy form %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{% block post_form_content %}
|
||||||
|
{% endblock %}
|
@ -32,7 +32,7 @@
|
|||||||
<div class='modal-form-content'>
|
<div class='modal-form-content'>
|
||||||
</div>
|
</div>
|
||||||
<div class='modal-footer'>
|
<div class='modal-footer'>
|
||||||
<button type='button' class='btn btn-default' data-dismiss='modal'>Close</button>
|
<button type='button' class='btn btn-default' data-dismiss='modal'>Cancel</button>
|
||||||
<button type='button' class='btn btn-danger' id='modal-form-delete'>Delete</button>
|
<button type='button' class='btn btn-danger' id='modal-form-delete'>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user