mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 12:35:46 +00:00
Merge remote-tracking branch 'inventree/master'
This commit is contained in:
@ -231,4 +231,6 @@ IMPORT_EXPORT_USE_TRANSACTIONS = True
|
||||
|
||||
# Settings for dbbsettings app
|
||||
DBBACKUP_STORAGE = 'django.core.files.storage.FileSystemStorage'
|
||||
DBBACKUP_STORAGE_OPTIONS = {'location': tempfile.gettempdir()}
|
||||
DBBACKUP_STORAGE_OPTIONS = {
|
||||
'location': CONFIG.get('backup_dir', tempfile.gettempdir()),
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ from part.api import part_api_urls, bom_api_urls
|
||||
from company.api import company_api_urls
|
||||
from stock.api import stock_api_urls
|
||||
from build.api import build_api_urls
|
||||
from order.api import po_api_urls
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
@ -43,6 +44,7 @@ apipatterns = [
|
||||
url(r'^company/', include(company_api_urls)),
|
||||
url(r'^stock/', include(stock_api_urls)),
|
||||
url(r'^build/', include(build_api_urls)),
|
||||
url(r'^po/', include(po_api_urls)),
|
||||
|
||||
# User URLs
|
||||
url(r'^user/', include(user_urls)),
|
||||
|
@ -43,4 +43,8 @@ media_root: './media'
|
||||
static_root: './static'
|
||||
|
||||
# Logging options
|
||||
log_queries: False
|
||||
log_queries: False
|
||||
|
||||
# Backup options
|
||||
# Set the backup_dir parameter to store backup files in a specific location
|
||||
# backup_dir = "/home/me/inventree-backup/"
|
92
InvenTree/order/api.py
Normal file
92
InvenTree/order/api.py
Normal file
@ -0,0 +1,92 @@
|
||||
"""
|
||||
JSON API for the Order app
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import generics, permissions
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||
from .serializers import POSerializer, POLineItemSerializer
|
||||
|
||||
|
||||
class POList(generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of Order objects
|
||||
|
||||
- GET: Return list of PO objects (with filters)
|
||||
- POST: Create a new PurchaseOrder object
|
||||
"""
|
||||
|
||||
queryset = PurchaseOrder.objects.all()
|
||||
serializer_class = POSerializer
|
||||
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
]
|
||||
|
||||
filter_backends = [
|
||||
DjangoFilterBackend,
|
||||
]
|
||||
|
||||
filter_fields = [
|
||||
'supplier',
|
||||
]
|
||||
|
||||
|
||||
class PODetail(generics.RetrieveUpdateAPIView):
|
||||
""" API endpoint for detail view of a PurchaseOrder object """
|
||||
|
||||
queryset = PurchaseOrder.objects.all()
|
||||
serializer_class = POSerializer
|
||||
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated
|
||||
]
|
||||
|
||||
|
||||
class POLineItemList(generics.ListCreateAPIView):
|
||||
""" API endpoint for accessing a list of PO Line Item objects
|
||||
|
||||
- GET: Return a list of PO Line Item objects
|
||||
- POST: Create a new PurchaseOrderLineItem object
|
||||
"""
|
||||
|
||||
queryset = PurchaseOrderLineItem.objects.all()
|
||||
serializer_class = POLineItemSerializer
|
||||
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
]
|
||||
|
||||
filter_backends = [
|
||||
DjangoFilterBackend,
|
||||
]
|
||||
|
||||
filter_fields = [
|
||||
'order',
|
||||
'part'
|
||||
]
|
||||
|
||||
|
||||
class POLineItemDetail(generics.RetrieveUpdateAPIView):
|
||||
""" API endpoint for detail view of a PurchaseOrderLineItem object """
|
||||
|
||||
queryset = PurchaseOrderLineItem
|
||||
serializer_class = POLineItemSerializer
|
||||
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
]
|
||||
|
||||
|
||||
po_api_urls = [
|
||||
url(r'^order/(?P<pk>\d+)/?$', PODetail.as_view(), name='api-po-detail'),
|
||||
url(r'^order/?$', POList.as_view(), name='api-po-list'),
|
||||
|
||||
url(r'^line/(?P<pk>\d+)/?$', POLineItemDetail.as_view(), name='api-po-line-detail'),
|
||||
url(r'^line/?$', POLineItemList.as_view(), name='api-po-line-list'),
|
||||
]
|
48
InvenTree/order/serializers.py
Normal file
48
InvenTree/order/serializers.py
Normal file
@ -0,0 +1,48 @@
|
||||
"""
|
||||
JSON serializers for the Order API
|
||||
"""
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from InvenTree.serializers import InvenTreeModelSerializer
|
||||
|
||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||
|
||||
|
||||
class POSerializer(InvenTreeModelSerializer):
|
||||
""" Serializes an Order object """
|
||||
|
||||
class Meta:
|
||||
model = PurchaseOrder
|
||||
|
||||
fields = [
|
||||
'pk',
|
||||
'supplier',
|
||||
'reference',
|
||||
'description',
|
||||
'URL',
|
||||
'status',
|
||||
'notes',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'reference',
|
||||
'status'
|
||||
]
|
||||
|
||||
|
||||
class POLineItemSerializer(InvenTreeModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = PurchaseOrderLineItem
|
||||
|
||||
fields = [
|
||||
'pk',
|
||||
'quantity',
|
||||
'reference',
|
||||
'notes',
|
||||
'order',
|
||||
'part',
|
||||
'received',
|
||||
]
|
@ -634,7 +634,8 @@ class Part(models.Model):
|
||||
For hash is calculated from the following fields of each BOM item:
|
||||
|
||||
- Part.full_name (if the part name changes, the BOM checksum is invalidated)
|
||||
- quantity
|
||||
- Quantity
|
||||
- Reference field
|
||||
- Note field
|
||||
|
||||
returns a string representation of a hash object which can be compared with a stored value
|
||||
@ -647,6 +648,7 @@ class Part(models.Model):
|
||||
hash.update(str(item.sub_part.full_name).encode())
|
||||
hash.update(str(item.quantity).encode())
|
||||
hash.update(str(item.note).encode())
|
||||
hash.update(str(item.reference).encode())
|
||||
|
||||
return str(hash.digest())
|
||||
|
||||
|
@ -102,7 +102,7 @@
|
||||
<td>
|
||||
<h4>Available Stock</h4>
|
||||
</td>
|
||||
<td><h4>{{ part.net_stock }} {{ part.units }}</h4></td>
|
||||
<td><h4>{{ part.available_stock }} {{ part.units }}</h4></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>In Stock</td>
|
||||
|
@ -118,11 +118,12 @@ class EditStockItemForm(HelperForm):
|
||||
|
||||
fields = [
|
||||
'supplier_part',
|
||||
'serial',
|
||||
'batch',
|
||||
'delete_on_deplete',
|
||||
'status',
|
||||
'notes',
|
||||
'URL',
|
||||
'delete_on_deplete',
|
||||
]
|
||||
|
||||
|
||||
|
@ -122,6 +122,11 @@ class StockItem(models.Model):
|
||||
system=True
|
||||
)
|
||||
|
||||
@property
|
||||
def serialized(self):
|
||||
""" Return True if this StockItem is serialized """
|
||||
return self.serial is not None and self.quantity == 1
|
||||
|
||||
@classmethod
|
||||
def check_serial_number(cls, part, serial_number):
|
||||
""" Check if a new stock item can be created with the provided part_id
|
||||
@ -190,20 +195,21 @@ class StockItem(models.Model):
|
||||
})
|
||||
|
||||
if self.part is not None:
|
||||
# A trackable part must have a serial number
|
||||
if self.part.trackable:
|
||||
if not self.serial:
|
||||
raise ValidationError({'serial': _('Serial number must be set for trackable items')})
|
||||
# A part with a serial number MUST have the quantity set to 1
|
||||
if self.serial is not None:
|
||||
if self.quantity > 1:
|
||||
raise ValidationError({
|
||||
'quantity': _('Quantity must be 1 for item with a serial number'),
|
||||
'serial': _('Serial number cannot be set if quantity greater than 1')
|
||||
})
|
||||
|
||||
if self.quantity == 0:
|
||||
raise ValidationError({
|
||||
'quantity': _('Quantity must be 1 for item with a serial number')
|
||||
})
|
||||
|
||||
if self.delete_on_deplete:
|
||||
raise ValidationError({'delete_on_deplete': _("Must be set to False for trackable items")})
|
||||
|
||||
# Serial number cannot be set for items with quantity greater than 1
|
||||
if not self.quantity == 1:
|
||||
raise ValidationError({
|
||||
'quantity': _("Quantity must be set to 1 for item with a serial number"),
|
||||
'serial': _("Serial number cannot be set if quantity > 1")
|
||||
})
|
||||
raise ValidationError({'delete_on_deplete': _("Must be set to False for item with a serial number")})
|
||||
|
||||
# A template part cannot be instantiated as a StockItem
|
||||
if self.part.is_template:
|
||||
@ -316,7 +322,15 @@ class StockItem(models.Model):
|
||||
infinite = models.BooleanField(default=False)
|
||||
|
||||
def can_delete(self):
|
||||
# TODO - Return FALSE if this item cannot be deleted!
|
||||
""" Can this stock item be deleted? It can NOT be deleted under the following circumstances:
|
||||
|
||||
- Has a serial number and is tracked
|
||||
- Is installed inside another StockItem
|
||||
"""
|
||||
|
||||
if self.part.trackable and self.serial is not None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@property
|
||||
@ -349,6 +363,14 @@ class StockItem(models.Model):
|
||||
|
||||
track.save()
|
||||
|
||||
@transaction.atomic
|
||||
def serializeStock(self, serials, user):
|
||||
""" Split this stock item into unique serial numbers.
|
||||
"""
|
||||
|
||||
# TODO
|
||||
pass
|
||||
|
||||
@transaction.atomic
|
||||
def splitStock(self, quantity, user):
|
||||
""" Split this stock item into two items, in the same location.
|
||||
@ -363,6 +385,10 @@ class StockItem(models.Model):
|
||||
The new item will have a different StockItem ID, while this will remain the same.
|
||||
"""
|
||||
|
||||
# Do not split a serialized part
|
||||
if self.serialized:
|
||||
return
|
||||
|
||||
# Doesn't make sense for a zero quantity
|
||||
if quantity <= 0:
|
||||
return
|
||||
@ -377,6 +403,8 @@ class StockItem(models.Model):
|
||||
quantity=quantity,
|
||||
supplier_part=self.supplier_part,
|
||||
location=self.location,
|
||||
notes=self.notes,
|
||||
URL=self.URL,
|
||||
batch=self.batch,
|
||||
delete_on_deplete=self.delete_on_deplete
|
||||
)
|
||||
@ -412,7 +440,7 @@ class StockItem(models.Model):
|
||||
if location is None:
|
||||
# TODO - Raise appropriate error (cannot move to blank location)
|
||||
return False
|
||||
elif self.location and (location.pk == self.location.pk):
|
||||
elif self.location and (location.pk == self.location.pk) and (quantity == self.quantity):
|
||||
# TODO - Raise appropriate error (cannot move to same location)
|
||||
return False
|
||||
|
||||
@ -450,12 +478,16 @@ class StockItem(models.Model):
|
||||
- False if the StockItem was deleted
|
||||
"""
|
||||
|
||||
# Do not adjust quantity of a serialized part
|
||||
if self.serialized:
|
||||
return
|
||||
|
||||
if quantity < 0:
|
||||
quantity = 0
|
||||
|
||||
self.quantity = quantity
|
||||
|
||||
if quantity <= 0 and self.delete_on_deplete:
|
||||
if quantity <= 0 and self.delete_on_deplete and self.can_delete():
|
||||
self.delete()
|
||||
return False
|
||||
else:
|
||||
@ -493,6 +525,10 @@ class StockItem(models.Model):
|
||||
or by manually adding the items to the stock location
|
||||
"""
|
||||
|
||||
# Cannot add items to a serialized part
|
||||
if self.serialized:
|
||||
return False
|
||||
|
||||
quantity = int(quantity)
|
||||
|
||||
# Ignore amounts that do not make sense
|
||||
@ -513,6 +549,10 @@ class StockItem(models.Model):
|
||||
""" Remove items from stock
|
||||
"""
|
||||
|
||||
# Cannot remove items from a serialized part
|
||||
if self.serialized:
|
||||
return False
|
||||
|
||||
quantity = int(quantity)
|
||||
|
||||
if quantity <= 0 or self.infinite:
|
||||
|
@ -5,11 +5,16 @@
|
||||
<div class='row'>
|
||||
<div class='col-sm-6'>
|
||||
<h3>Stock Item Details</h3>
|
||||
{% if item.serialized %}
|
||||
<p><i>{{ item.part.full_name}} # {{ item.serial }}</i></p>
|
||||
{% else %}
|
||||
<p><i>{{ item.quantity }} × {{ item.part.full_name }}</i></p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<div class='btn-group'>
|
||||
{% include "qr_button.html" %}
|
||||
{% if item.in_stock %}
|
||||
{% if not item.serialized %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='stock-add' title='Add to stock'>
|
||||
<span class='glyphicon glyphicon-plus-sign' style='color: #1a1;'/>
|
||||
</button>
|
||||
@ -19,6 +24,7 @@
|
||||
<button type='button' class='btn btn-default btn-glyph' id='stock-count' title='Count stock'>
|
||||
<span class='glyphicon glyphicon-ok-circle'/>
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type='button' class='btn btn-default btn-glyph' id='stock-move' title='Transfer stock'>
|
||||
<span class='glyphicon glyphicon-transfer' style='color: #11a;'/>
|
||||
</button>
|
||||
@ -34,6 +40,11 @@
|
||||
</button>
|
||||
</div>
|
||||
</p>
|
||||
{% if item.serialized %}
|
||||
<div class='alert alert-block alert-info'>
|
||||
This stock item is serialized - it has a unique serial number and the quantity cannot be adjusted.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class='row'>
|
||||
@ -41,7 +52,10 @@
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<td>Part</td>
|
||||
<td><a href="{% url 'part-stock' item.part.id %}">{{ item.part.full_name }}</td>
|
||||
<td>
|
||||
{% include "hover_image.html" with image=item.part.image hover=True %}
|
||||
<a href="{% url 'part-stock' item.part.id %}">{{ item.part.full_name }}
|
||||
</td>
|
||||
</tr>
|
||||
{% if item.belongs_to %}
|
||||
<tr>
|
||||
@ -54,9 +68,9 @@
|
||||
<td><a href="{% url 'stock-location-detail' item.location.id %}">{{ item.location.name }}</a></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% if item.serial %}
|
||||
{% if item.serialized %}
|
||||
<tr>
|
||||
<td>Serial</td>
|
||||
<td>Serial Number</td>
|
||||
<td>{{ item.serial }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
|
@ -383,8 +383,8 @@ class StockAdjust(AjaxView, FormMixin):
|
||||
if item.new_quantity <= 0:
|
||||
continue
|
||||
|
||||
# Do not move to the same location
|
||||
if destination == item.location:
|
||||
# Do not move to the same location (unless the quantity is different)
|
||||
if destination == item.location and item.new_quantity == item.quantity:
|
||||
continue
|
||||
|
||||
item.move(destination, note, self.request.user, quantity=int(item.new_quantity))
|
||||
@ -429,6 +429,9 @@ class StockItemEdit(AjaxUpdateView):
|
||||
query = query.filter(part=item.part.id)
|
||||
form.fields['supplier_part'].queryset = query
|
||||
|
||||
if not item.part.trackable:
|
||||
form.fields.pop('serial')
|
||||
|
||||
return form
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user