Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2020-11-19 11:29:14 +11:00
commit 5b35915f33
11 changed files with 157 additions and 144 deletions

View File

@ -182,6 +182,7 @@ class StockStatus(StatusCode):
ATTENTION: 'yellow', ATTENTION: 'yellow',
DAMAGED: 'red', DAMAGED: 'red',
DESTROYED: 'red', DESTROYED: 'red',
LOST: 'grey',
REJECTED: 'red', REJECTED: 'red',
} }

View File

@ -388,14 +388,15 @@ class AjaxCreateView(AjaxMixin, CreateView):
# Save the object to the database # Save the object to the database
self.object = self.save(self.form) self.object = self.save(self.form)
# Return the PK of the newly-created object if self.object:
data['pk'] = self.object.pk # Return the PK of the newly-created object
data['text'] = str(self.object) data['pk'] = self.object.pk
data['text'] = str(self.object)
try: try:
data['url'] = self.object.get_absolute_url() data['url'] = self.object.get_absolute_url()
except AttributeError: except AttributeError:
pass pass
return self.renderJsonResponse(request, self.form, data) return self.renderJsonResponse(request, self.form, data)

View File

@ -67,7 +67,7 @@ def migrate_currencies(apps, schema_editor):
currency_code = remap.get(currency_id, 'USD') currency_code = remap.get(currency_id, 'USD')
# Update the currency code # Update the currency code
response = cursor.execute(f'UPDATE part_supplierpricebreak set price_currency= "{currency_code}" where id={pk};') response = cursor.execute(f"UPDATE part_supplierpricebreak set price_currency= '{currency_code}' where id={pk};")
count += 1 count += 1
@ -105,7 +105,7 @@ def reverse_currencies(apps, schema_editor):
# For each currency code in use, check if we have a matching Currency object # For each currency code in use, check if we have a matching Currency object
for code in codes_in_use: for code in codes_in_use:
response = cursor.execute(f'SELECT id, suffix from common_currency where suffix="{code}";') response = cursor.execute(f"SELECT id, suffix from common_currency where suffix='{code}';")
row = response.fetchone() row = response.fetchone()
if row is not None: if row is not None:
@ -133,7 +133,7 @@ def reverse_currencies(apps, schema_editor):
# Update the table to point to the Currency objects # Update the table to point to the Currency objects
print(f"Currency {suffix} -> pk {pk}") print(f"Currency {suffix} -> pk {pk}")
response = cursor.execute(f'UPDATE part_supplierpricebreak set currency_id={pk} where price_currency="{suffix}";') response = cursor.execute(f"UPDATE part_supplierpricebreak set currency_id={pk} where price_currency='{suffix}';")
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -113,9 +113,9 @@ class CategoryDetail(generics.RetrieveUpdateDestroyAPIView):
class CategoryParameters(generics.ListAPIView): class CategoryParameters(generics.ListAPIView):
""" API endpoint for accessing a list of PartCategory objects. """ API endpoint for accessing a list of PartCategoryParameterTemplate objects.
- GET: Return a list of PartCategory objects - GET: Return a list of PartCategoryParameterTemplate objects
""" """
queryset = PartCategoryParameterTemplate.objects.all() queryset = PartCategoryParameterTemplate.objects.all()
@ -124,21 +124,36 @@ class CategoryParameters(generics.ListAPIView):
def get_queryset(self): def get_queryset(self):
""" """
Custom filtering: Custom filtering:
- Allow filtering by "null" parent to retrieve top-level part categories - Allow filtering by "null" parent to retrieve all categories parameter templates
- Allow filtering by category
- Allow traversing all parent categories
""" """
cat_id = self.kwargs.get('pk', None) try:
cat_id = int(self.request.query_params.get('category', None))
except TypeError:
cat_id = None
fetch_parent = str2bool(self.request.query_params.get('fetch_parent', 'true'))
queryset = super().get_queryset() queryset = super().get_queryset()
if cat_id is not None: if isinstance(cat_id, int):
try:
cat_id = int(cat_id)
queryset = queryset.filter(category=cat_id)
except ValueError:
pass
try:
category = PartCategory.objects.get(pk=cat_id)
except PartCategory.DoesNotExist:
# Return empty queryset
return PartCategoryParameterTemplate.objects.none()
category_list = [cat_id]
if fetch_parent:
parent_categories = category.get_ancestors()
for parent in parent_categories:
category_list.append(parent.pk)
queryset = queryset.filter(category__in=category_list)
return queryset return queryset
@ -895,8 +910,8 @@ part_api_urls = [
# Base URL for PartCategory API endpoints # Base URL for PartCategory API endpoints
url(r'^category/', include([ url(r'^category/', include([
url(r'^(?P<pk>\d+)/parameters/?', CategoryParameters.as_view(), name='api-part-category-parameters'),
url(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'), url(r'^(?P<pk>\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'),
url(r'^parameters/?', CategoryParameters.as_view(), name='api-part-category-parameters'),
url(r'^$', CategoryList.as_view(), name='api-part-category-list'), url(r'^$', CategoryList.as_view(), name='api-part-category-list'),
])), ])),

View File

@ -67,7 +67,7 @@ def migrate_currencies(apps, schema_editor):
currency_code = remap.get(currency_id, 'USD') currency_code = remap.get(currency_id, 'USD')
# Update the currency code # Update the currency code
response = cursor.execute(f'UPDATE part_partsellpricebreak set price_currency= "{currency_code}" where id={pk};') response = cursor.execute(f"UPDATE part_partsellpricebreak set price_currency='{currency_code}' where id={pk};")
count += 1 count += 1
@ -105,7 +105,7 @@ def reverse_currencies(apps, schema_editor):
# For each currency code in use, check if we have a matching Currency object # For each currency code in use, check if we have a matching Currency object
for code in codes_in_use: for code in codes_in_use:
response = cursor.execute(f'SELECT id, suffix from common_currency where suffix="{code}";') response = cursor.execute(f"SELECT id, suffix from common_currency where suffix='{code}';")
row = response.fetchone() row = response.fetchone()
if row is not None: if row is not None:
@ -120,7 +120,7 @@ def reverse_currencies(apps, schema_editor):
print(f"Creating new Currency object for {code}") print(f"Creating new Currency object for {code}")
# Construct a query to create a new Currency object # Construct a query to create a new Currency object
query = f'INSERT into common_currency (symbol, suffix, description, value, base) VALUES ("$", "{code}", "{description}", 1.0, False);' query = f"INSERT into common_currency (symbol, suffix, description, value, base) VALUES ('$', '{code}', '{description}', 1.0, False);"
response = cursor.execute(query) response = cursor.execute(query)
@ -133,7 +133,7 @@ def reverse_currencies(apps, schema_editor):
# Update the table to point to the Currency objects # Update the table to point to the Currency objects
print(f"Currency {suffix} -> pk {pk}") print(f"Currency {suffix} -> pk {pk}")
response = cursor.execute(f'UPDATE part_partsellpricebreak set currency_id={pk} where price_currency="{suffix}";') response = cursor.execute(f"UPDATE part_partsellpricebreak set currency_id={pk} where price_currency='{suffix}';")
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -423,9 +423,8 @@ class PartParameterTemplateSerializer(InvenTreeModelSerializer):
class CategoryParameterTemplateSerializer(InvenTreeModelSerializer): class CategoryParameterTemplateSerializer(InvenTreeModelSerializer):
""" Serializer for PartCategoryParameterTemplate """ """ Serializer for PartCategoryParameterTemplate """
parameter_template_detail = PartParameterTemplateSerializer(source='parameter_template', parameter_template = PartParameterTemplateSerializer(many=False,
many=False, read_only=True)
read_only=True)
class Meta: class Meta:
model = PartCategoryParameterTemplate model = PartCategoryParameterTemplate
@ -433,6 +432,5 @@ class CategoryParameterTemplateSerializer(InvenTreeModelSerializer):
'pk', 'pk',
'category', 'category',
'parameter_template', 'parameter_template',
'parameter_template_detail',
'default_value', 'default_value',
] ]

View File

@ -124,11 +124,11 @@ class CreateStockItemForm(HelperForm):
fields = [ fields = [
'part', 'part',
'supplier_part', 'supplier_part',
'purchase_price',
'location', 'location',
'quantity', 'quantity',
'batch', 'batch',
'serial_numbers', 'serial_numbers',
'purchase_price',
'link', 'link',
'delete_on_deplete', 'delete_on_deplete',
'status', 'status',

View File

@ -0,0 +1,19 @@
# Generated by Django 3.0.7 on 2020-11-17 03:53
from django.db import migrations
import djmoney.models.fields
class Migration(migrations.Migration):
dependencies = [
('stock', '0054_remove_stockitem_build_order'),
]
operations = [
migrations.AlterField(
model_name='stockitem',
name='purchase_price',
field=djmoney.models.fields.MoneyField(blank=True, decimal_places=4, default_currency='USD', help_text='Single unit purchase price at time of purchase', max_digits=19, null=True, verbose_name='Purchase Price'),
),
]

View File

@ -453,6 +453,7 @@ class StockItem(MPTTModel):
max_digits=19, max_digits=19,
decimal_places=4, decimal_places=4,
default_currency='USD', default_currency='USD',
blank=True,
null=True, null=True,
verbose_name=_('Purchase Price'), verbose_name=_('Purchase Price'),
help_text=_('Single unit purchase price at time of purchase'), help_text=_('Single unit purchase price at time of purchase'),

View File

@ -1607,127 +1607,101 @@ class StockItemCreate(AjaxCreateView):
return initials return initials
def post(self, request, *args, **kwargs): def validate(self, item, form):
""" Handle POST of StockItemCreate form. """
Extra form validation steps
- Manage serial-number valdiation for tracked parts
""" """
part = None data = form.cleaned_data
form = self.get_form() part = data['part']
quantity = data.get('quantity', None)
data = {} if not quantity:
return
valid = form.is_valid() try:
quantity = Decimal(quantity)
except (ValueError, InvalidOperation):
form.add_error('quantity', _('Invalid quantity provided'))
return
if valid: if quantity < 0:
part_id = form['part'].value() form.add_error('quantity', _('Quantity cannot be negative'))
try:
part = Part.objects.get(id=part_id)
quantity = Decimal(form['quantity'].value())
except (Part.DoesNotExist, ValueError, InvalidOperation): # Trackable parts are treated differently
part = None if part.trackable:
quantity = 1 sn = data.get('serial_numbers', '')
valid = False sn = str(sn).strip()
form.add_error('quantity', _('Invalid quantity'))
if quantity < 0: if len(sn) > 0:
form.add_error('quantity', _('Quantity cannot be less than zero')) serials = extract_serial_numbers(sn, quantity)
valid = False
if part is None: existing = part.find_conflicting_serial_numbers(serials)
form.add_error('part', _('Invalid part selection'))
if len(existing) > 0:
exists = ','.join([str(x) for x in existing])
form.add_error(
'serial_numbers',
_('Serial numbers already exist') + ': ' + exists
)
def save(self, form, **kwargs):
"""
Create a new StockItem based on the provided form data.
"""
data = form.cleaned_data
part = data['part']
quantity = data['quantity']
if part.trackable:
sn = data.get('serial_numbers', '')
sn = str(sn).strip()
# Create a single stock item for each provided serial number
if len(sn) > 0:
serials = extract_serial_numbers(sn, quantity)
for serial in serials:
item = StockItem(
part=part,
quantity=1,
serial=serial,
supplier_part=data.get('supplier_part', None),
location=data.get('location', None),
batch=data.get('batch', None),
delete_on_deplete=False,
status=data.get('status'),
link=data.get('link', ''),
)
item.save(user=self.request.user)
# Create a single StockItem of the specified quantity
else: else:
# A trackable part must provide serial numbesr form._post_clean()
if part.trackable:
sn = request.POST.get('serial_numbers', '')
sn = str(sn).strip() item = form.save(commit=False)
item.user = self.request.user
item.save()
# If user has specified a range of serial numbers return item
if len(sn) > 0:
try: # Non-trackable part
serials = extract_serial_numbers(sn, quantity) else:
existing = part.find_conflicting_serial_numbers(serials) form._post_clean()
item = form.save(commit=False)
item.user = self.request.user
item.save()
if len(existing) > 0: return item
exists = ",".join([str(x) for x in existing])
form.add_error(
'serial_numbers',
_('Serial numbers already exist') + ': ' + exists
)
valid = False
else:
# At this point we have a list of serial numbers which we know are valid,
# and do not currently exist
form.clean()
form_data = form.cleaned_data
if form.is_valid():
for serial in serials:
# Create a new stock item for each serial number
item = StockItem(
part=part,
quantity=1,
serial=serial,
supplier_part=form_data.get('supplier_part'),
location=form_data.get('location'),
batch=form_data.get('batch'),
delete_on_deplete=False,
status=form_data.get('status'),
link=form_data.get('link'),
)
item.save(user=request.user)
data['success'] = _('Created {n} new stock items'.format(n=len(serials)))
valid = True
except ValidationError as e:
form.add_error('serial_numbers', e.messages)
valid = False
else:
# We have a serialized part, but no serial numbers specified...
form.clean()
form._post_clean()
if form.is_valid():
item = form.save(commit=False)
item.save(user=request.user)
data['pk'] = item.pk
data['url'] = item.get_absolute_url()
data['success'] = _("Created new stock item")
valid = True
else: # Referenced Part object is not marked as "trackable"
# For non-serialized items, simply save the form.
# We need to call _post_clean() here because it is prevented in the form implementation
form.clean()
form._post_clean()
if form.is_valid:
item = form.save(commit=False)
item.save(user=request.user)
data['pk'] = item.pk
data['url'] = item.get_absolute_url()
data['success'] = _("Created new stock item")
valid = True
data['form_valid'] = valid and form.is_valid()
return self.renderJsonResponse(request, form, data=data)
class StockLocationDelete(AjaxDeleteView): class StockLocationDelete(AjaxDeleteView):

View File

@ -536,13 +536,17 @@ function loadStockTable(table, options) {
// Special stock status codes // Special stock status codes
// 65 = "REJECTED" // REJECTED
if (row.status == 65) { if (row.status == {{ StockStatus.REJECTED }}) {
html += makeIconButton('fa-times-circle icon-red', '{% trans "Stock item has been rejected" %}'); console.log("REJECTED - {{ StockStatus.REJECTED }}");
html += makeIconBadge('fa-times-circle icon-red', '{% trans "Stock item has been rejected" %}');
} }
// 70 = "LOST" // LOST
else if (row.status == 70) { else if (row.status == {{ StockStatus.LOST }}) {
html += makeIconButton('fa-question-circle','{% trans "Stock item is lost" %}'); html += makeIconBadge('fa-question-circle','{% trans "Stock item is lost" %}');
}
else if (row.status == {{ StockStatus.DESTROYED }}) {
html += makeIconBadge('fa-skull-crossbones', '{% trans "Stock item is destroyed" %}');
} }
if (row.quantity <= 0) { if (row.quantity <= 0) {