mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
commit
5b35915f33
@ -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',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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,20 +124,35 @@ 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:
|
try:
|
||||||
cat_id = int(cat_id)
|
category = PartCategory.objects.get(pk=cat_id)
|
||||||
queryset = queryset.filter(category=cat_id)
|
except PartCategory.DoesNotExist:
|
||||||
except ValueError:
|
# Return empty queryset
|
||||||
pass
|
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'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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',
|
||||||
]
|
]
|
||||||
|
@ -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',
|
||||||
|
19
InvenTree/stock/migrations/0055_auto_20201117_1453.py
Normal file
19
InvenTree/stock/migrations/0055_auto_20201117_1453.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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'),
|
||||||
|
@ -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']
|
||||||
|
|
||||||
data = {}
|
quantity = data.get('quantity', None)
|
||||||
|
|
||||||
valid = form.is_valid()
|
if not quantity:
|
||||||
|
return
|
||||||
|
|
||||||
if valid:
|
try:
|
||||||
part_id = form['part'].value()
|
quantity = Decimal(quantity)
|
||||||
try:
|
except (ValueError, InvalidOperation):
|
||||||
part = Part.objects.get(id=part_id)
|
form.add_error('quantity', _('Invalid quantity provided'))
|
||||||
quantity = Decimal(form['quantity'].value())
|
return
|
||||||
|
|
||||||
except (Part.DoesNotExist, ValueError, InvalidOperation):
|
if quantity < 0:
|
||||||
part = None
|
form.add_error('quantity', _('Quantity cannot be negative'))
|
||||||
quantity = 1
|
|
||||||
valid = False
|
|
||||||
form.add_error('quantity', _('Invalid quantity'))
|
|
||||||
|
|
||||||
if quantity < 0:
|
# Trackable parts are treated differently
|
||||||
form.add_error('quantity', _('Quantity cannot be less than zero'))
|
if part.trackable:
|
||||||
valid = False
|
sn = data.get('serial_numbers', '')
|
||||||
|
sn = str(sn).strip()
|
||||||
|
|
||||||
if part is None:
|
if len(sn) > 0:
|
||||||
form.add_error('part', _('Invalid part selection'))
|
serials = extract_serial_numbers(sn, quantity)
|
||||||
|
|
||||||
|
existing = part.find_conflicting_serial_numbers(serials)
|
||||||
|
|
||||||
|
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:
|
|
||||||
serials = extract_serial_numbers(sn, quantity)
|
|
||||||
|
|
||||||
existing = part.find_conflicting_serial_numbers(serials)
|
# Non-trackable part
|
||||||
|
else:
|
||||||
|
|
||||||
if len(existing) > 0:
|
form._post_clean()
|
||||||
exists = ",".join([str(x) for x in existing])
|
|
||||||
form.add_error(
|
|
||||||
'serial_numbers',
|
|
||||||
_('Serial numbers already exist') + ': ' + exists
|
|
||||||
)
|
|
||||||
valid = False
|
|
||||||
|
|
||||||
else:
|
item = form.save(commit=False)
|
||||||
# At this point we have a list of serial numbers which we know are valid,
|
item.user = self.request.user
|
||||||
# and do not currently exist
|
item.save()
|
||||||
form.clean()
|
|
||||||
|
|
||||||
form_data = form.cleaned_data
|
return item
|
||||||
|
|
||||||
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):
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user