2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 03:00:54 +00:00

Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters
2020-05-26 12:52:17 +10:00
40 changed files with 1199 additions and 210 deletions

View File

@ -80,21 +80,10 @@ class StockDetail(generics.RetrieveUpdateDestroyAPIView):
def get_serializer(self, *args, **kwargs):
try:
kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', False))
except AttributeError:
pass
try:
kwargs['location_detail'] = str2bool(self.request.query_params.get('location_detail', False))
except AttributeError:
pass
try:
kwargs['supplier_part_detail'] = str2bool(self.request.query_params.get('supplier_detail', False))
except AttributeError:
pass
kwargs['part_detail'] = True
kwargs['location_detail'] = True
kwargs['supplier_part_detail'] = True
kwargs['test_detail'] = True
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
@ -498,8 +487,21 @@ class StockList(generics.ListCreateAPIView):
if serial_number is not None:
queryset = queryset.filter(serial=serial_number)
# Filter by range of serial numbers?
serial_number_gte = params.get('serial_gte', None)
serial_number_lte = params.get('serial_lte', None)
if serial_number_gte is not None or serial_number_lte is not None:
queryset = queryset.exclude(serial=None)
if serial_number_gte is not None:
queryset = queryset.filter(serial__gte=serial_number_gte)
in_stock = self.request.query_params.get('in_stock', None)
if serial_number_lte is not None:
queryset = queryset.filter(serial__lte=serial_number_lte)
in_stock = params.get('in_stock', None)
if in_stock is not None:
in_stock = str2bool(in_stock)
@ -512,7 +514,7 @@ class StockList(generics.ListCreateAPIView):
queryset = queryset.exclude(StockItem.IN_STOCK_FILTER)
# Filter by 'allocated' patrs?
allocated = self.request.query_params.get('allocated', None)
allocated = params.get('allocated', None)
if allocated is not None:
allocated = str2bool(allocated)
@ -531,8 +533,14 @@ class StockList(generics.ListCreateAPIView):
active = str2bool(active)
queryset = queryset.filter(part__active=active)
# Filter by internal part number
IPN = params.get('IPN', None)
if IPN:
queryset = queryset.filter(part__IPN=IPN)
# Does the client wish to filter by the Part ID?
part_id = self.request.query_params.get('part', None)
part_id = params.get('part', None)
if part_id:
try:
@ -692,17 +700,14 @@ class StockItemTestResultList(generics.ListCreateAPIView):
'value',
]
ordering = 'date'
def get_serializer(self, *args, **kwargs):
try:
kwargs['user_detail'] = str2bool(self.request.query_params.get('user_detail', False))
except:
pass
try:
kwargs['attachment_detail'] = str2bool(self.request.query_params.get('attachment_detail', False))
except:
pass
kwargs['context'] = self.get_serializer_context()
return self.serializer_class(*args, **kwargs)
@ -718,23 +723,6 @@ class StockItemTestResultList(generics.ListCreateAPIView):
# Capture the user information
test_result = serializer.save()
test_result.user = self.request.user
# Check if a file has been attached to the request
attachment_file = self.request.FILES.get('attachment', None)
if attachment_file:
# Create a new attachment associated with the stock item
attachment = StockItemAttachment(
attachment=attachment_file,
stock_item=test_result.stock_item,
user=test_result.user
)
attachment.save()
# Link the attachment back to the test result
test_result.attachment = attachment
test_result.save()

View File

@ -63,6 +63,18 @@ class EditStockLocationForm(HelperForm):
]
class ConvertStockItemForm(HelperForm):
"""
Form for converting a StockItem to a variant of its current part.
"""
class Meta:
model = StockItem
fields = [
'part'
]
class CreateStockItemForm(HelperForm):
""" Form for creating a new StockItem """
@ -142,6 +154,34 @@ class SerializeStockForm(HelperForm):
]
class TestReportFormatForm(HelperForm):
""" Form for selection a test report template """
class Meta:
model = StockItem
fields = [
'template',
]
def __init__(self, stock_item, *args, **kwargs):
self.stock_item = stock_item
super().__init__(*args, **kwargs)
self.fields['template'].choices = self.get_template_choices()
def get_template_choices(self):
""" Available choices """
choices = []
for report in self.stock_item.part.get_test_report_templates():
choices.append((report.pk, report))
return choices
template = forms.ChoiceField(label=_('Template'), help_text=_('Select test report template'))
class ExportOptionsForm(HelperForm):
""" Form for selecting stock export options """

View File

@ -0,0 +1,19 @@
# Generated by Django 3.0.5 on 2020-05-23 01:21
from django.db import migrations, models
import stock.models
class Migration(migrations.Migration):
dependencies = [
('stock', '0041_stockitemtestresult_notes'),
]
operations = [
migrations.AlterField(
model_name='stockitemtestresult',
name='attachment',
field=models.FileField(blank=True, help_text='Test result attachment', null=True, upload_to=stock.models.rename_stock_item_test_result_attachment, verbose_name='Attachment'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.0.5 on 2020-05-25 04:20
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('part', '0042_auto_20200518_0900'),
('stock', '0042_auto_20200523_0121'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='part',
field=models.ForeignKey(help_text='Base part', limit_choices_to={'active': True, 'virtual': False}, on_delete=django.db.models.deletion.CASCADE, related_name='stock_items', to='part.Part', verbose_name='Base Part'),
),
]

View File

@ -331,7 +331,6 @@ class StockItem(MPTTModel):
verbose_name=_('Base Part'),
related_name='stock_items', help_text=_('Base part'),
limit_choices_to={
'is_template': False,
'active': True,
'virtual': False
})
@ -647,6 +646,9 @@ class StockItem(MPTTModel):
# Copy entire transaction history
new_item.copyHistoryFrom(self)
# Copy test result history
new_item.copyTestResultsFrom(self)
# Create a new stock tracking item
new_item.addTransactionNote(_('Add serial number'), user, notes=notes)
@ -655,7 +657,7 @@ class StockItem(MPTTModel):
@transaction.atomic
def copyHistoryFrom(self, other):
""" Copy stock history from another part """
""" Copy stock history from another StockItem """
for item in other.tracking_info.all():
@ -663,6 +665,17 @@ class StockItem(MPTTModel):
item.pk = None
item.save()
@transaction.atomic
def copyTestResultsFrom(self, other, filters={}):
""" Copy all test results from another StockItem """
for result in other.test_results.all().filter(**filters):
# Create a copy of the test result by nulling-out the pk
result.pk = None
result.stock_item = self
result.save()
@transaction.atomic
def splitStock(self, quantity, location, user):
""" Split this stock item into two items, in the same location.
@ -713,6 +726,9 @@ class StockItem(MPTTModel):
# Copy the transaction history of this part into the new one
new_stock.copyHistoryFrom(self)
# Copy the test results of this part to the new one
new_stock.copyTestResultsFrom(self)
# Add a new tracking item for the new stock item
new_stock.addTransactionNote(
"Split from existing stock",
@ -963,6 +979,13 @@ class StockItem(MPTTModel):
return result_map
def testResultList(self, **kwargs):
"""
Return a list of test-result objects for this StockItem
"""
return self.testResultMap(**kwargs).values()
def requiredTestStatus(self):
"""
Return the status of the tests required for this StockItem.
@ -1000,6 +1023,10 @@ class StockItem(MPTTModel):
'failed': failed,
}
@property
def required_test_count(self):
return self.part.getRequiredTests().count()
def hasRequiredTests(self):
return self.part.getRequiredTests().count() > 0
@ -1083,6 +1110,11 @@ class StockItemTracking(models.Model):
# file = models.FileField()
def rename_stock_item_test_result_attachment(instance, filename):
return os.path.join('stock_files', str(instance.stock_item.pk), os.path.basename(filename))
class StockItemTestResult(models.Model):
"""
A StockItemTestResult records results of custom tests against individual StockItem objects.
@ -1102,19 +1134,41 @@ class StockItemTestResult(models.Model):
date: Date the test result was recorded
"""
def save(self, *args, **kwargs):
super().clean()
super().validate_unique()
super().save(*args, **kwargs)
def clean(self):
super().clean()
# If an attachment is linked to this result, the attachment must also point to the item
try:
if self.attachment:
if not self.attachment.stock_item == self.stock_item:
raise ValidationError({
'attachment': _("Test result attachment must be linked to the same StockItem"),
})
except (StockItem.DoesNotExist, StockItemAttachment.DoesNotExist):
pass
# If this test result corresponds to a template, check the requirements of the template
key = self.key
templates = self.stock_item.part.getTestTemplates()
for template in templates:
if key == template.key:
if template.requires_value:
if not self.value:
raise ValidationError({
"value": _("Value must be provided for this test"),
})
if template.requires_attachment:
if not self.attachment:
raise ValidationError({
"attachment": _("Attachment must be uploaded for this test"),
})
break
@property
def key(self):
return helpers.generateTestKey(self.test)
stock_item = models.ForeignKey(
StockItem,
@ -1140,10 +1194,9 @@ class StockItemTestResult(models.Model):
help_text=_('Test output value')
)
attachment = models.ForeignKey(
StockItemAttachment,
on_delete=models.SET_NULL,
blank=True, null=True,
attachment = models.FileField(
null=True, blank=True,
upload_to=rename_stock_item_test_result_attachment,
verbose_name=_('Attachment'),
help_text=_('Test result attachment'),
)

View File

@ -108,11 +108,14 @@ class StockItemSerializer(InvenTreeModelSerializer):
quantity = serializers.FloatField()
allocated = serializers.FloatField()
required_tests = serializers.IntegerField(source='required_test_count', read_only=True)
def __init__(self, *args, **kwargs):
part_detail = kwargs.pop('part_detail', False)
location_detail = kwargs.pop('location_detail', False)
supplier_part_detail = kwargs.pop('supplier_part_detail', False)
test_detail = kwargs.pop('test_detail', False)
super(StockItemSerializer, self).__init__(*args, **kwargs)
@ -125,6 +128,9 @@ class StockItemSerializer(InvenTreeModelSerializer):
if supplier_part_detail is not True:
self.fields.pop('supplier_part_detail')
if test_detail is not True:
self.fields.pop('required_tests')
class Meta:
model = StockItem
fields = [
@ -141,6 +147,7 @@ class StockItemSerializer(InvenTreeModelSerializer):
'part_detail',
'pk',
'quantity',
'required_tests',
'sales_order',
'serial',
'supplier_part',
@ -222,31 +229,28 @@ class StockItemTestResultSerializer(InvenTreeModelSerializer):
""" Serializer for the StockItemTestResult model """
user_detail = UserSerializerBrief(source='user', read_only=True)
attachment_detail = StockItemAttachmentSerializer(source='attachment', read_only=True)
key = serializers.CharField(read_only=True)
def __init__(self, *args, **kwargs):
user_detail = kwargs.pop('user_detail', False)
attachment_detail = kwargs.pop('attachment_detail', False)
super().__init__(*args, **kwargs)
if user_detail is not True:
self.fields.pop('user_detail')
if attachment_detail is not True:
self.fields.pop('attachment_detail')
class Meta:
model = StockItemTestResult
fields = [
'pk',
'stock_item',
'key',
'test',
'result',
'value',
'attachment',
'attachment_detail',
'notes',
'user',
'user_detail',
@ -255,7 +259,6 @@ class StockItemTestResultSerializer(InvenTreeModelSerializer):
read_only_fields = [
'pk',
'attachment',
'user',
'date',
]

View File

@ -93,8 +93,18 @@ InvenTree | {% trans "Stock Item" %} - {{ item }}
<span class='fas fa-copy'/>
</button>
{% endif %}
{% if item.part.has_variants %}
<button type='button' class='btn btn-default' id='stock-convert' title="Convert stock to variant">
<span class='fas fa-screwdriver'/>
</button>
{% endif %}
{% if item.part.has_test_report_templates %}
<button type='button' class='btn btn-default' id='stock-test-report' title='Generate test report'>
<span class='fas fa-tasks'/>
</button>
{% endif %}
<button type='button' class='btn btn-default' id='stock-edit' title='Edit stock item'>
<span class='fas fa-edit'/>
<span class='fas fa-edit icon-blue'/>
</button>
{% if item.can_delete %}
<button type='button' class='btn btn-default' id='stock-delete' title='Edit stock item'>
@ -264,6 +274,17 @@ $("#stock-serialize").click(function() {
);
});
{% if item.part.has_test_report_templates %}
$("#stock-test-report").click(function() {
launchModalForm(
"{% url 'stock-item-test-report-select' item.id %}",
{
follow: true,
}
);
});
{% endif %}
$("#stock-duplicate").click(function() {
launchModalForm(
"{% url 'stock-item-create' %}",
@ -308,6 +329,16 @@ function itemAdjust(action) {
);
}
{% if item.part.has_variants %}
$("#stock-convert").click(function() {
launchModalForm("{% url 'stock-item-convert' item.id %}",
{
reload: true,
}
);
});
{% endif %}
$("#stock-move").click(function() {
itemAdjust("move");
});

View File

@ -13,7 +13,13 @@
<div id='button-toolbar'>
<div class='button-toolbar container-fluid' style="float: right;">
<div class='btn-group' role='group'>
{% if user.is_staff %}
<button type='button' class='btn btn-danger' id='delete-test-results'>{% trans "Delete Test Data" %}</button>
{% endif %}
<button type='button' class='btn btn-success' id='add-test-result'>{% trans "Add Test Data" %}</button>
{% if item.part.has_test_report_templates %}
<button type='button' class='btn btn-default' id='test-report'>{% trans "Test Report" %} <span class='fas fa-tasks'></span></button>
{% endif %}
</div>
<div class='filter-list' id='filter-list-stocktests'>
<!-- Empty div -->
@ -40,6 +46,28 @@ function reloadTable() {
//$("#test-result-table").bootstrapTable("refresh");
}
{% if item.part.has_test_report_templates %}
$("#test-report").click(function() {
launchModalForm(
"{% url 'stock-item-test-report-select' item.id %}",
{
follow: true,
}
);
});
{% endif %}
{% if user.is_staff %}
$("#delete-test-results").click(function() {
launchModalForm(
"{% url 'stock-item-delete-test-data' item.id %}",
{
success: reloadTable,
}
);
});
{% endif %}
$("#add-test-result").click(function() {
launchModalForm(
"{% url 'stock-item-test-create' %}", {

View File

@ -32,7 +32,7 @@
<input class='numberinput'
min='0'
{% if stock_action == 'take' or stock_action == 'move' %} max='{{ item.quantity }}' {% endif %}
value='{{ item.new_quantity }}' type='number' name='stock-id-{{ item.id }}' id='stock-id-{{ item.id }}'/>
value='{% decimal item.new_quantity %}' type='number' name='stock-id-{{ item.id }}' id='stock-id-{{ item.id }}'/>
{% if item.error %}
<br><span class='help-inline'>{{ item.error }}</span>
{% endif %}

View File

@ -0,0 +1,17 @@
{% extends "modal_form.html" %}
{% load i18n %}
{% block pre_form_content %}
<div class='alert alert-block alert-info'>
<b>{% trans "Convert Stock Item" %}</b><br>
{% trans "This stock item is current an instance of " %}<i>{{ item.part }}</i><br>
{% trans "It can be converted to one of the part variants listed below." %}
</div>
<div class='alert alert-block alert-warning'>
<b>{% trans "Warning" %}</b>
{% trans "This action cannot be easily undone" %}
</div>
{% endblock %}

View File

@ -458,3 +458,68 @@ class TestResultTest(StockTest):
)
self.assertTrue(item.passedAllRequiredTests())
def test_duplicate_item_tests(self):
# Create an example stock item by copying one from the database (because we are lazy)
item = StockItem.objects.get(pk=522)
item.pk = None
item.serial = None
item.quantity = 50
item.save()
# Do some tests!
StockItemTestResult.objects.create(
stock_item=item,
test="Firmware",
result=True
)
StockItemTestResult.objects.create(
stock_item=item,
test="Paint Color",
result=True,
value="Red"
)
StockItemTestResult.objects.create(
stock_item=item,
test="Applied Sticker",
result=False
)
self.assertEqual(item.test_results.count(), 3)
self.assertEqual(item.quantity, 50)
# Split some items out
item2 = item.splitStock(20, None, None)
self.assertEqual(item.quantity, 30)
self.assertEqual(item.test_results.count(), 3)
self.assertEqual(item2.test_results.count(), 3)
StockItemTestResult.objects.create(
stock_item=item2,
test='A new test'
)
self.assertEqual(item.test_results.count(), 3)
self.assertEqual(item2.test_results.count(), 4)
# Test StockItem serialization
item2.serializeStock(1, [100], self.user)
# Add a test result to the parent *after* serialization
StockItemTestResult.objects.create(
stock_item=item2,
test='abcde'
)
self.assertEqual(item2.test_results.count(), 5)
item3 = StockItem.objects.get(serial=100, part=item2.part)
self.assertEqual(item3.test_results.count(), 4)

View File

@ -18,12 +18,16 @@ stock_location_detail_urls = [
stock_item_detail_urls = [
url(r'^edit/', views.StockItemEdit.as_view(), name='stock-item-edit'),
url(r'^convert/', views.StockItemConvert.as_view(), name='stock-item-convert'),
url(r'^serialize/', views.StockItemSerialize.as_view(), name='stock-item-serialize'),
url(r'^delete/', views.StockItemDelete.as_view(), name='stock-item-delete'),
url(r'^qr_code/', views.StockItemQRCode.as_view(), name='stock-item-qr'),
url(r'^delete_test_data/', views.StockItemDeleteTestData.as_view(), name='stock-item-delete-test-data'),
url(r'^add_tracking/', views.StockItemTrackingCreate.as_view(), name='stock-tracking-create'),
url(r'^test-report-select/', views.StockItemTestReportSelect.as_view(), name='stock-item-test-report-select'),
url(r'^test/', views.StockItemDetail.as_view(template_name='stock/item_tests.html'), name='stock-item-test-results'),
url(r'^children/', views.StockItemDetail.as_view(template_name='stock/item_childs.html'), name='stock-item-children'),
url(r'^attachments/', views.StockItemDetail.as_view(template_name='stock/item_attachments.html'), name='stock-item-attachments'),
@ -52,6 +56,8 @@ stock_urls = [
url(r'^item/new/?', views.StockItemCreate.as_view(), name='stock-item-create'),
url(r'^item/test-report-download/', views.StockItemTestReportDownload.as_view(), name='stock-item-test-report-download'),
# URLs for StockItem attachments
url(r'^item/attachment/', include([
url(r'^new/', views.StockItemAttachmentCreate.as_view(), name='stock-item-attachment-create'),

View File

@ -17,6 +17,7 @@ from django.utils.translation import ugettext as _
from InvenTree.views import AjaxView
from InvenTree.views import AjaxUpdateView, AjaxDeleteView, AjaxCreateView
from InvenTree.views import QRCodeView
from InvenTree.forms import ConfirmForm
from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats
from InvenTree.helpers import ExtractSerialNumbers
@ -26,19 +27,12 @@ from datetime import datetime
from company.models import Company, SupplierPart
from part.models import Part
from report.models import TestReport
from .models import StockItem, StockLocation, StockItemTracking, StockItemAttachment, StockItemTestResult
from .admin import StockItemResource
from .forms import EditStockLocationForm
from .forms import CreateStockItemForm
from .forms import EditStockItemForm
from .forms import AdjustStockForm
from .forms import TrackingEntryForm
from .forms import SerializeStockForm
from .forms import ExportOptionsForm
from .forms import EditStockItemAttachmentForm
from .forms import EditStockItemTestResultForm
from . import forms as StockForms
class StockIndex(ListView):
@ -113,7 +107,7 @@ class StockLocationEdit(AjaxUpdateView):
"""
model = StockLocation
form_class = EditStockLocationForm
form_class = StockForms.EditStockLocationForm
context_object_name = 'location'
ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit Stock Location')
@ -157,7 +151,7 @@ class StockItemAttachmentCreate(AjaxCreateView):
"""
model = StockItemAttachment
form_class = EditStockItemAttachmentForm
form_class = StockForms.EditStockItemAttachmentForm
ajax_form_title = _("Add Stock Item Attachment")
ajax_template_name = "modal_form.html"
@ -202,7 +196,7 @@ class StockItemAttachmentEdit(AjaxUpdateView):
"""
model = StockItemAttachment
form_class = EditStockItemAttachmentForm
form_class = StockForms.EditStockItemAttachmentForm
ajax_form_title = _("Edit Stock Item Attachment")
def get_form(self):
@ -229,13 +223,48 @@ class StockItemAttachmentDelete(AjaxDeleteView):
}
class StockItemDeleteTestData(AjaxUpdateView):
"""
View for deleting all test data
"""
model = StockItem
form_class = ConfirmForm
ajax_form_title = _("Delete All Test Data")
def get_form(self):
return ConfirmForm()
def post(self, request, *args, **kwargs):
valid = False
stock_item = StockItem.objects.get(pk=self.kwargs['pk'])
form = self.get_form()
confirm = str2bool(request.POST.get('confirm', False))
if confirm is not True:
form.errors['confirm'] = [_('Confirm test data deletion')]
form.non_field_errors = [_('Check the confirmation box')]
else:
stock_item.test_results.all().delete()
valid = True
data = {
'form_valid': valid,
}
return self.renderJsonResponse(request, form, data)
class StockItemTestResultCreate(AjaxCreateView):
"""
View for adding a new StockItemTestResult
"""
model = StockItemTestResult
form_class = EditStockItemTestResultForm
form_class = StockForms.EditStockItemTestResultForm
ajax_form_title = _("Add Test Result")
def post_save(self, **kwargs):
@ -263,17 +292,6 @@ class StockItemTestResultCreate(AjaxCreateView):
form = super().get_form()
form.fields['stock_item'].widget = HiddenInput()
# Extract the StockItem object
item_id = form['stock_item'].value()
# Limit the options for the file attachments
try:
stock_item = StockItem.objects.get(pk=item_id)
form.fields['attachment'].queryset = stock_item.attachments.all()
except (ValueError, StockItem.DoesNotExist):
# Hide the attachments field
form.fields['attachment'].widget = HiddenInput()
return form
@ -283,7 +301,7 @@ class StockItemTestResultEdit(AjaxUpdateView):
"""
model = StockItemTestResult
form_class = EditStockItemTestResultForm
form_class = StockForms.EditStockItemTestResultForm
ajax_form_title = _("Edit Test Result")
def get_form(self):
@ -291,8 +309,6 @@ class StockItemTestResultEdit(AjaxUpdateView):
form = super().get_form()
form.fields['stock_item'].widget = HiddenInput()
form.fields['attachment'].queryset = self.object.stock_item.attachments.all()
return form
@ -307,12 +323,81 @@ class StockItemTestResultDelete(AjaxDeleteView):
context_object_name = "result"
class StockItemTestReportSelect(AjaxView):
"""
View for selecting a TestReport template,
and generating a TestReport as a PDF.
"""
model = StockItem
ajax_form_title = _("Select Test Report Template")
def get_form(self):
stock_item = StockItem.objects.get(pk=self.kwargs['pk'])
return StockForms.TestReportFormatForm(stock_item)
def post(self, request, *args, **kwargs):
template_id = request.POST.get('template', None)
try:
template = TestReport.objects.get(pk=template_id)
except (ValueError, TestReport.DoesNoteExist):
raise ValidationError({'template': _("Select valid template")})
stock_item = StockItem.objects.get(pk=self.kwargs['pk'])
url = reverse('stock-item-test-report-download')
url += '?stock_item={id}'.format(id=stock_item.pk)
url += '&template={id}'.format(id=template.pk)
data = {
'form_valid': True,
'url': url,
}
return self.renderJsonResponse(request, self.get_form(), data=data)
class StockItemTestReportDownload(AjaxView):
"""
Download a TestReport against a StockItem.
Requires the following arguments to be passed as URL params:
stock_item - Valid PK of a StockItem object
template - Valid PK of a TestReport template object
"""
def get(self, request, *args, **kwargs):
template = request.GET.get('template', None)
stock_item = request.GET.get('stock_item', None)
try:
template = TestReport.objects.get(pk=template)
except (ValueError, TestReport.DoesNotExist):
raise ValidationError({'template': 'Invalid template ID'})
try:
stock_item = StockItem.objects.get(pk=stock_item)
except (ValueError, StockItem.DoesNotExist):
raise ValidationError({'stock_item': 'Invalid StockItem ID'})
template.stock_item = stock_item
return template.render(request)
class StockExportOptions(AjaxView):
""" Form for selecting StockExport options """
model = StockLocation
ajax_form_title = _('Stock Export Options')
form_class = ExportOptionsForm
form_class = StockForms.ExportOptionsForm
def post(self, request, *args, **kwargs):
@ -455,7 +540,7 @@ class StockAdjust(AjaxView, FormMixin):
ajax_template_name = 'stock/stock_adjust.html'
ajax_form_title = _('Adjust Stock')
form_class = AdjustStockForm
form_class = StockForms.AdjustStockForm
stock_items = []
def get_GET_items(self):
@ -773,7 +858,7 @@ class StockItemEdit(AjaxUpdateView):
"""
model = StockItem
form_class = EditStockItemForm
form_class = StockForms.EditStockItemForm
context_object_name = 'item'
ajax_template_name = 'modal_form.html'
ajax_form_title = _('Edit Stock Item')
@ -802,6 +887,30 @@ class StockItemEdit(AjaxUpdateView):
return form
class StockItemConvert(AjaxUpdateView):
"""
View for 'converting' a StockItem to a variant of its current part.
"""
model = StockItem
form_class = StockForms.ConvertStockItemForm
ajax_form_title = _('Convert Stock Item')
ajax_template_name = 'stock/stockitem_convert.html'
context_object_name = 'item'
def get_form(self):
"""
Filter the available parts.
"""
form = super().get_form()
item = self.get_object()
form.fields['part'].queryset = item.part.get_all_variants()
return form
class StockLocationCreate(AjaxCreateView):
"""
View for creating a new StockLocation
@ -809,7 +918,7 @@ class StockLocationCreate(AjaxCreateView):
"""
model = StockLocation
form_class = EditStockLocationForm
form_class = StockForms.EditStockLocationForm
context_object_name = 'location'
ajax_template_name = 'modal_form.html'
ajax_form_title = _('Create new Stock Location')
@ -834,7 +943,7 @@ class StockItemSerialize(AjaxUpdateView):
model = StockItem
ajax_template_name = 'stock/item_serialize.html'
ajax_form_title = _('Serialize Stock')
form_class = SerializeStockForm
form_class = StockForms.SerializeStockForm
def get_form(self):
@ -843,7 +952,7 @@ class StockItemSerialize(AjaxUpdateView):
# Pass the StockItem object through to the form
context['item'] = self.get_object()
form = SerializeStockForm(**context)
form = StockForms.SerializeStockForm(**context)
return form
@ -922,11 +1031,41 @@ class StockItemCreate(AjaxCreateView):
"""
model = StockItem
form_class = CreateStockItemForm
form_class = StockForms.CreateStockItemForm
context_object_name = 'item'
ajax_template_name = 'modal_form.html'
ajax_form_title = _('Create new Stock Item')
def get_part(self, form=None):
"""
Attempt to get the "part" associted with this new stockitem.
- May be passed to the form as a query parameter (e.g. ?part=<id>)
- May be passed via the form field itself.
"""
# Try to extract from the URL query
part_id = self.request.GET.get('part', None)
if part_id:
try:
part = Part.objects.get(pk=part_id)
return part
except (Part.DoesNotExist, ValueError):
pass
# Try to get from the form
if form:
try:
part_id = form['part'].value()
part = Part.objects.get(pk=part_id)
return part
except (Part.DoesNotExist, ValueError):
pass
# Could not extract a part object
return None
def get_form(self):
""" Get form for StockItem creation.
Overrides the default get_form() method to intelligently limit
@ -935,53 +1074,44 @@ class StockItemCreate(AjaxCreateView):
form = super().get_form()
part = None
part = self.get_part(form=form)
# If the user has selected a Part, limit choices for SupplierPart
if form['part'].value():
part_id = form['part'].value()
if part is not None:
sn = part.getNextSerialNumber()
form.field_placeholder['serial_numbers'] = _('Next available serial number is') + ' ' + str(sn)
try:
part = Part.objects.get(id=part_id)
sn = part.getNextSerialNumber()
form.field_placeholder['serial_numbers'] = _('Next available serial number is') + ' ' + str(sn)
form.rebuild_layout()
form.rebuild_layout()
# Hide the 'part' field (as a valid part is selected)
form.fields['part'].widget = HiddenInput()
# Hide the 'part' field (as a valid part is selected)
form.fields['part'].widget = HiddenInput()
# trackable parts get special consideration
if part.trackable:
form.fields['delete_on_deplete'].widget = HiddenInput()
form.fields['delete_on_deplete'].initial = False
else:
form.fields.pop('serial_numbers')
# trackable parts get special consideration
if part.trackable:
form.fields['delete_on_deplete'].widget = HiddenInput()
form.fields['delete_on_deplete'].initial = False
else:
form.fields.pop('serial_numbers')
# If the part is NOT purchaseable, hide the supplier_part field
if not part.purchaseable:
form.fields['supplier_part'].widget = HiddenInput()
else:
# Pre-select the allowable SupplierPart options
parts = form.fields['supplier_part'].queryset
parts = parts.filter(part=part.id)
# If the part is NOT purchaseable, hide the supplier_part field
if not part.purchaseable:
form.fields['supplier_part'].widget = HiddenInput()
else:
# Pre-select the allowable SupplierPart options
parts = form.fields['supplier_part'].queryset
parts = parts.filter(part=part.id)
form.fields['supplier_part'].queryset = parts
form.fields['supplier_part'].queryset = parts
# If there is one (and only one) supplier part available, pre-select it
all_parts = parts.all()
# If there is one (and only one) supplier part available, pre-select it
all_parts = parts.all()
if len(all_parts) == 1:
if len(all_parts) == 1:
# TODO - This does NOT work for some reason? Ref build.views.BuildItemCreate
form.fields['supplier_part'].initial = all_parts[0].id
except Part.DoesNotExist:
pass
# TODO - This does NOT work for some reason? Ref build.views.BuildItemCreate
form.fields['supplier_part'].initial = all_parts[0].id
# Otherwise if the user has selected a SupplierPart, we know what Part they meant!
elif form['supplier_part'].value() is not None:
if form['supplier_part'].value() is not None:
pass
return form
@ -1004,27 +1134,20 @@ class StockItemCreate(AjaxCreateView):
else:
initials = super(StockItemCreate, self).get_initial().copy()
part_id = self.request.GET.get('part', None)
part = self.get_part()
loc_id = self.request.GET.get('location', None)
sup_part_id = self.request.GET.get('supplier_part', None)
part = None
location = None
supplier_part = None
# Part field has been specified
if part_id:
try:
part = Part.objects.get(pk=part_id)
# Check that the supplied part is 'valid'
if not part.is_template and part.active and not part.virtual:
initials['part'] = part
initials['location'] = part.get_default_location()
initials['supplier_part'] = part.default_supplier
except (ValueError, Part.DoesNotExist):
pass
if part is not None:
# Check that the supplied part is 'valid'
if not part.is_template and part.active and not part.virtual:
initials['part'] = part
initials['location'] = part.get_default_location()
initials['supplier_part'] = part.default_supplier
# SupplierPart field has been specified
# It must match the Part, if that has been supplied
@ -1229,7 +1352,7 @@ class StockItemTrackingEdit(AjaxUpdateView):
model = StockItemTracking
ajax_form_title = _('Edit Stock Tracking Entry')
form_class = TrackingEntryForm
form_class = StockForms.TrackingEntryForm
class StockItemTrackingCreate(AjaxCreateView):
@ -1238,7 +1361,7 @@ class StockItemTrackingCreate(AjaxCreateView):
model = StockItemTracking
ajax_form_title = _("Add Stock Tracking Entry")
form_class = TrackingEntryForm
form_class = StockForms.TrackingEntryForm
def post(self, request, *args, **kwargs):