Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2019-08-02 22:37:26 +10:00
commit 84ea95181d
15 changed files with 256 additions and 35 deletions

View File

@ -10,7 +10,8 @@ addons:
-sqlite3
before_install:
- make install
- make requirements
- make secret
- make migrate
script:

View File

@ -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()),
}

View File

@ -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)),

View File

@ -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
View 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'),
]

View 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',
]

View File

@ -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())

View File

@ -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>

View File

@ -118,11 +118,12 @@ class EditStockItemForm(HelperForm):
fields = [
'supplier_part',
'serial',
'batch',
'delete_on_deplete',
'status',
'notes',
'URL',
'delete_on_deplete',
]

View File

@ -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:

View File

@ -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 }} &times {{ 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 %}

View File

@ -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

View File

@ -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

View File

@ -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.

View 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)