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
84ea95181d
@ -10,7 +10,8 @@ addons:
|
||||
-sqlite3
|
||||
|
||||
before_install:
|
||||
- make install
|
||||
- make requirements
|
||||
- make secret
|
||||
- make migrate
|
||||
|
||||
script:
|
||||
|
@ -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)),
|
||||
|
@ -44,3 +44,7 @@ static_root: './static'
|
||||
|
||||
# Logging options
|
||||
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
|
||||
|
||||
|
||||
|
8
Makefile
8
Makefile
@ -16,13 +16,17 @@ migrate:
|
||||
python3 InvenTree/manage.py migrate --run-syncdb
|
||||
python3 InvenTree/manage.py check
|
||||
|
||||
install:
|
||||
requirements:
|
||||
pip3 install -U -r requirements.txt
|
||||
|
||||
secret:
|
||||
python3 InvenTree/keygen.py
|
||||
|
||||
superuser:
|
||||
python3 InvenTree/manage.py createsuperuser
|
||||
|
||||
install: requirements secret migrate superuser
|
||||
|
||||
style:
|
||||
flake8 InvenTree
|
||||
|
||||
@ -42,3 +46,5 @@ documentation:
|
||||
backup:
|
||||
python3 InvenTree/manage.py dbbackup
|
||||
python3 InvenTree/manage.py mediabackup
|
||||
|
||||
.PHONY: clean migrate requirements secret superuser install style test coverage documentation backup
|
@ -48,3 +48,8 @@ Uploaded File Storage
|
||||
---------------------
|
||||
|
||||
By default, uploaded files are stored in the local direction ``./media``. This directory should be changed based on the particular installation requirements.
|
||||
|
||||
Backup Location
|
||||
---------------
|
||||
|
||||
The default behaviour of the database backup is to generate backup files for database tables and media files to the user's temporary directory. The target directory can be overridden by setting the *backup_dir* parameter in the config file.
|
@ -24,6 +24,8 @@ which performs the following actions:
|
||||
|
||||
* Installs all required Python packages using pip package manager
|
||||
* Generates a SECREY_KEY file required for the django authentication framework
|
||||
* Performs initial database installation and migrations
|
||||
* Prompts user to create a superuser account
|
||||
|
||||
Install Configuration
|
||||
---------------------
|
||||
@ -34,15 +36,10 @@ The configuration file provides administrators control over various setup option
|
||||
|
||||
For further information on installation configuration, refer to the `Configuration <config.html>`_ section.
|
||||
|
||||
Superuser Account
|
||||
-----------------
|
||||
|
||||
Run ``make superuser`` to create a superuser account, required for initial system login.
|
||||
|
||||
Run Development Server
|
||||
----------------------
|
||||
|
||||
Run ``python3 InvenTree/manage.py runserver`` to launch a development server. This will launch the InvenTree web interface at ``127.0.0.1:8000``. For other options refer to the `django docs <https://docs.djangoproject.com/en/2.2/ref/django-admin/>`_.
|
||||
Run ``python3 InvenTree/manage.py runserver 127.0.0.1:8000`` to launch a development server. This will launch the InvenTree web interface at ``127.0.0.1:8000``. For other options refer to the `django docs <https://docs.djangoproject.com/en/2.2/ref/django-admin/>`_.
|
||||
|
||||
Database Migrations
|
||||
-------------------
|
||||
@ -54,6 +51,10 @@ Development and Testing
|
||||
|
||||
Other shorthand functions are provided for the development and testing process:
|
||||
|
||||
* ``make requirements`` - Install all required underlying packages using PIP
|
||||
* ``make secret`` - Generate the SECRET_KEY file for session validation
|
||||
* ``make superuser`` - Create a superuser account
|
||||
* ``make backup`` - Backup database tables and media files
|
||||
* ``make test`` - Run all unit tests
|
||||
* ``make coverage`` - Run all unit tests and generate code coverage report
|
||||
* ``make style`` - Check Python codebase against PEP coding standards (using Flake)
|
||||
|
Loading…
Reference in New Issue
Block a user