mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'inventree:master' into part-import
This commit is contained in:
commit
e77e89b16c
3
.gitignore
vendored
3
.gitignore
vendored
@ -62,3 +62,6 @@ secret_key.txt
|
|||||||
# Coverage reports
|
# Coverage reports
|
||||||
.coverage
|
.coverage
|
||||||
htmlcov/
|
htmlcov/
|
||||||
|
|
||||||
|
# Development files
|
||||||
|
dev/
|
@ -26,10 +26,9 @@ def canAppAccessDatabase():
|
|||||||
'flush',
|
'flush',
|
||||||
'loaddata',
|
'loaddata',
|
||||||
'dumpdata',
|
'dumpdata',
|
||||||
'makemirations',
|
'makemigrations',
|
||||||
'migrate',
|
'migrate',
|
||||||
'check',
|
'check',
|
||||||
'mediarestore',
|
|
||||||
'shell',
|
'shell',
|
||||||
'createsuperuser',
|
'createsuperuser',
|
||||||
'wait_for_db',
|
'wait_for_db',
|
||||||
|
@ -98,7 +98,7 @@ DOCKER = _is_true(get_setting(
|
|||||||
# Configure logging settings
|
# Configure logging settings
|
||||||
log_level = get_setting(
|
log_level = get_setting(
|
||||||
'INVENTREE_LOG_LEVEL',
|
'INVENTREE_LOG_LEVEL',
|
||||||
CONFIG.get('log_level', 'DEBUG')
|
CONFIG.get('log_level', 'WARNING')
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@ -192,7 +192,7 @@ STATIC_URL = '/static/'
|
|||||||
STATIC_ROOT = os.path.abspath(
|
STATIC_ROOT = os.path.abspath(
|
||||||
get_setting(
|
get_setting(
|
||||||
'INVENTREE_STATIC_ROOT',
|
'INVENTREE_STATIC_ROOT',
|
||||||
CONFIG.get('static_root', '/home/inventree/static')
|
CONFIG.get('static_root', '/home/inventree/data/static')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ from django.conf.urls.static import static
|
|||||||
from django.views.generic.base import RedirectView
|
from django.views.generic.base import RedirectView
|
||||||
from rest_framework.documentation import include_docs_urls
|
from rest_framework.documentation import include_docs_urls
|
||||||
|
|
||||||
|
from .views import auth_request
|
||||||
from .views import IndexView, SearchView, DatabaseStatsView
|
from .views import IndexView, SearchView, DatabaseStatsView
|
||||||
from .views import SettingsView, EditUserView, SetPasswordView
|
from .views import SettingsView, EditUserView, SetPasswordView
|
||||||
from .views import CurrencySettingsView, CurrencyRefreshView
|
from .views import CurrencySettingsView, CurrencyRefreshView
|
||||||
@ -155,20 +156,24 @@ urlpatterns = [
|
|||||||
url(r'^search/', SearchView.as_view(), name='search'),
|
url(r'^search/', SearchView.as_view(), name='search'),
|
||||||
url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
|
url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
|
||||||
|
|
||||||
|
url(r'^auth/?', auth_request),
|
||||||
|
|
||||||
url(r'^api/', include(apipatterns)),
|
url(r'^api/', include(apipatterns)),
|
||||||
url(r'^api-doc/', include_docs_urls(title='InvenTree API')),
|
url(r'^api-doc/', include_docs_urls(title='InvenTree API')),
|
||||||
|
|
||||||
url(r'^markdownx/', include('markdownx.urls')),
|
url(r'^markdownx/', include('markdownx.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Static file access
|
# Server running in "DEBUG" mode?
|
||||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
if settings.DEBUG:
|
||||||
|
# Static file access
|
||||||
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
|
||||||
# Media file access
|
# Media file access
|
||||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
# Debug toolbar access (if in DEBUG mode)
|
# Debug toolbar access (only allowed in DEBUG mode)
|
||||||
if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS:
|
if 'debug_toolbar' in settings.INSTALLED_APPS:
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('__debug/', include(debug_toolbar.urls)),
|
path('__debug/', include(debug_toolbar.urls)),
|
||||||
|
@ -8,7 +8,7 @@ import re
|
|||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
|
|
||||||
INVENTREE_SW_VERSION = "0.2.3 pre"
|
INVENTREE_SW_VERSION = "0.2.4 pre"
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment thi API version number whenever there is a significant change to the API that any clients need to know about
|
Increment thi API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
@ -10,7 +10,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.http import JsonResponse, HttpResponseRedirect
|
from django.http import HttpResponse, JsonResponse, HttpResponseRedirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
@ -36,6 +36,19 @@ from .helpers import str2bool
|
|||||||
from rest_framework import views
|
from rest_framework import views
|
||||||
|
|
||||||
|
|
||||||
|
def auth_request(request):
|
||||||
|
"""
|
||||||
|
Simple 'auth' endpoint used to determine if the user is authenticated.
|
||||||
|
Useful for (for example) redirecting authentication requests through
|
||||||
|
django's permission framework.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if request.user.is_authenticated:
|
||||||
|
return HttpResponse(status=200)
|
||||||
|
else:
|
||||||
|
return HttpResponse(status=403)
|
||||||
|
|
||||||
|
|
||||||
class TreeSerializer(views.APIView):
|
class TreeSerializer(views.APIView):
|
||||||
""" JSON View for serializing a Tree object.
|
""" JSON View for serializing a Tree object.
|
||||||
|
|
||||||
|
@ -1289,10 +1289,23 @@ class BuildItem(models.Model):
|
|||||||
Return qualified URL for part thumbnail image
|
Return qualified URL for part thumbnail image
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
thumb_url = None
|
||||||
|
|
||||||
if self.stock_item and self.stock_item.part:
|
if self.stock_item and self.stock_item.part:
|
||||||
return InvenTree.helpers.getMediaUrl(self.stock_item.part.image.thumbnail.url)
|
try:
|
||||||
elif self.bom_item and self.stock_item.sub_part:
|
# Try to extract the thumbnail
|
||||||
return InvenTree.helpers.getMediaUrl(self.bom_item.sub_part.image.thumbnail.url)
|
thumb_url = self.stock_item.part.image.thumbnail.url
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if thumb_url is None and self.bom_item and self.bom_item.sub_part:
|
||||||
|
try:
|
||||||
|
thumb_url = self.bom_item.sub_part.image.thumbnail.url
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if thumb_url is not None:
|
||||||
|
return InvenTree.helpers.getMediaUrl(thumb_url)
|
||||||
else:
|
else:
|
||||||
return InvenTree.helpers.getBlankThumbnail()
|
return InvenTree.helpers.getBlankThumbnail()
|
||||||
|
|
||||||
|
@ -52,3 +52,10 @@
|
|||||||
part: 2
|
part: 2
|
||||||
supplier: 2
|
supplier: 2
|
||||||
SKU: 'ZERGM312'
|
SKU: 'ZERGM312'
|
||||||
|
|
||||||
|
- model: company.supplierpart
|
||||||
|
pk: 5
|
||||||
|
fields:
|
||||||
|
part: 4
|
||||||
|
supplier: 2
|
||||||
|
SKU: 'R_4K7_0603'
|
||||||
|
@ -65,7 +65,7 @@ class CompanySimpleTest(TestCase):
|
|||||||
self.assertEqual(acme.supplied_part_count, 4)
|
self.assertEqual(acme.supplied_part_count, 4)
|
||||||
|
|
||||||
self.assertTrue(appel.has_parts)
|
self.assertTrue(appel.has_parts)
|
||||||
self.assertEqual(appel.supplied_part_count, 3)
|
self.assertEqual(appel.supplied_part_count, 4)
|
||||||
|
|
||||||
self.assertTrue(zerg.has_parts)
|
self.assertTrue(zerg.has_parts)
|
||||||
self.assertEqual(zerg.supplied_part_count, 2)
|
self.assertEqual(zerg.supplied_part_count, 2)
|
||||||
|
@ -129,9 +129,9 @@ cors:
|
|||||||
media_root: '/home/inventree/data/media'
|
media_root: '/home/inventree/data/media'
|
||||||
|
|
||||||
# STATIC_ROOT is the local filesystem location for storing static files
|
# STATIC_ROOT is the local filesystem location for storing static files
|
||||||
# By default, it is stored under /home/inventree
|
# By default, it is stored under /home/inventree/data/static
|
||||||
# Use environment variable INVENTREE_STATIC_ROOT
|
# Use environment variable INVENTREE_STATIC_ROOT
|
||||||
static_root: '/home/inventree/static'
|
static_root: '/home/inventree/data/static'
|
||||||
|
|
||||||
# Optional URL schemes to allow in URL fields
|
# Optional URL schemes to allow in URL fields
|
||||||
# By default, only the following schemes are allowed: ['http', 'https', 'ftp', 'ftps']
|
# By default, only the following schemes are allowed: ['http', 'https', 'ftp', 'ftps']
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -68,6 +68,7 @@
|
|||||||
order: 1
|
order: 1
|
||||||
part: 1
|
part: 1
|
||||||
quantity: 100
|
quantity: 100
|
||||||
|
destination: 5 # Desk/Drawer_1
|
||||||
|
|
||||||
# 250 x ACME0002 (M2x4 LPHS)
|
# 250 x ACME0002 (M2x4 LPHS)
|
||||||
# Partially received (50)
|
# Partially received (50)
|
||||||
@ -95,3 +96,10 @@
|
|||||||
part: 3
|
part: 3
|
||||||
quantity: 100
|
quantity: 100
|
||||||
|
|
||||||
|
# 1 x R_4K7_0603
|
||||||
|
- model: order.purchaseorderlineitem
|
||||||
|
pk: 23
|
||||||
|
fields:
|
||||||
|
order: 1
|
||||||
|
part: 5
|
||||||
|
quantity: 1
|
||||||
|
@ -86,12 +86,17 @@ class ShipSalesOrderForm(HelperForm):
|
|||||||
|
|
||||||
class ReceivePurchaseOrderForm(HelperForm):
|
class ReceivePurchaseOrderForm(HelperForm):
|
||||||
|
|
||||||
location = TreeNodeChoiceField(queryset=StockLocation.objects.all(), required=True, label=_('Location'), help_text=_('Receive parts to this location'))
|
location = TreeNodeChoiceField(
|
||||||
|
queryset=StockLocation.objects.all(),
|
||||||
|
required=True,
|
||||||
|
label=_("Destination"),
|
||||||
|
help_text=_("Receive parts to this location"),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrder
|
model = PurchaseOrder
|
||||||
fields = [
|
fields = [
|
||||||
'location',
|
"location",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -202,6 +207,7 @@ class EditPurchaseOrderLineItemForm(HelperForm):
|
|||||||
'quantity',
|
'quantity',
|
||||||
'reference',
|
'reference',
|
||||||
'purchase_price',
|
'purchase_price',
|
||||||
|
'destination',
|
||||||
'notes',
|
'notes',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 3.2 on 2021-05-13 22:38
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import django.db.models.deletion
|
||||||
|
import mptt.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("stock", "0063_auto_20210511_2343"),
|
||||||
|
("order", "0045_auto_20210504_1946"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="purchaseorderlineitem",
|
||||||
|
name="destination",
|
||||||
|
field=mptt.fields.TreeForeignKey(
|
||||||
|
blank=True,
|
||||||
|
help_text="Where does the Purchaser want this item to be stored?",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||||
|
related_name="po_lines",
|
||||||
|
to="stock.stocklocation",
|
||||||
|
verbose_name="Destination",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -20,6 +20,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from common.settings import currency_code_default
|
from common.settings import currency_code_default
|
||||||
|
|
||||||
from markdownx.models import MarkdownxField
|
from markdownx.models import MarkdownxField
|
||||||
|
from mptt.models import TreeForeignKey
|
||||||
|
|
||||||
from djmoney.models.fields import MoneyField
|
from djmoney.models.fields import MoneyField
|
||||||
|
|
||||||
@ -672,6 +673,29 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
help_text=_('Unit purchase price'),
|
help_text=_('Unit purchase price'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
destination = TreeForeignKey(
|
||||||
|
'stock.StockLocation', on_delete=models.DO_NOTHING,
|
||||||
|
verbose_name=_('Destination'),
|
||||||
|
related_name='po_lines',
|
||||||
|
blank=True, null=True,
|
||||||
|
help_text=_('Where does the Purchaser want this item to be stored?')
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_destination(self):
|
||||||
|
"""Show where the line item is or should be placed"""
|
||||||
|
# NOTE: If a line item gets split when recieved, only an arbitrary
|
||||||
|
# stock items location will be reported as the location for the
|
||||||
|
# entire line.
|
||||||
|
for stock in stock_models.StockItem.objects.filter(
|
||||||
|
supplier_part=self.part, purchase_order=self.order
|
||||||
|
):
|
||||||
|
if stock.location:
|
||||||
|
return stock.location
|
||||||
|
if self.destination:
|
||||||
|
return self.destination
|
||||||
|
if self.part and self.part.part and self.part.part.default_location:
|
||||||
|
return self.part.part.default_location
|
||||||
|
|
||||||
def remaining(self):
|
def remaining(self):
|
||||||
""" Calculate the number of items remaining to be received """
|
""" Calculate the number of items remaining to be received """
|
||||||
r = self.quantity - self.received
|
r = self.quantity - self.received
|
||||||
|
@ -17,6 +17,7 @@ from InvenTree.serializers import InvenTreeAttachmentSerializerField
|
|||||||
|
|
||||||
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
|
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
|
||||||
from part.serializers import PartBriefSerializer
|
from part.serializers import PartBriefSerializer
|
||||||
|
from stock.serializers import LocationBriefSerializer
|
||||||
|
|
||||||
from .models import PurchaseOrder, PurchaseOrderLineItem
|
from .models import PurchaseOrder, PurchaseOrderLineItem
|
||||||
from .models import PurchaseOrderAttachment, SalesOrderAttachment
|
from .models import PurchaseOrderAttachment, SalesOrderAttachment
|
||||||
@ -116,6 +117,8 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
purchase_price_string = serializers.CharField(source='purchase_price', read_only=True)
|
purchase_price_string = serializers.CharField(source='purchase_price', read_only=True)
|
||||||
|
|
||||||
|
destination = LocationBriefSerializer(source='get_destination', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PurchaseOrderLineItem
|
model = PurchaseOrderLineItem
|
||||||
|
|
||||||
@ -132,6 +135,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
|
|||||||
'purchase_price',
|
'purchase_price',
|
||||||
'purchase_price_currency',
|
'purchase_price_currency',
|
||||||
'purchase_price_string',
|
'purchase_price_string',
|
||||||
|
'destination',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -117,6 +117,7 @@ $("#po-table").inventreeTable({
|
|||||||
part_detail: true,
|
part_detail: true,
|
||||||
},
|
},
|
||||||
url: "{% url 'api-po-line-list' %}",
|
url: "{% url 'api-po-line-list' %}",
|
||||||
|
showFooter: true,
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
field: 'pk',
|
field: 'pk',
|
||||||
@ -137,6 +138,9 @@ $("#po-table").inventreeTable({
|
|||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
footerFormatter: function() {
|
||||||
|
return '{% trans "Total" %}'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'part_detail.description',
|
field: 'part_detail.description',
|
||||||
@ -172,7 +176,14 @@ $("#po-table").inventreeTable({
|
|||||||
{
|
{
|
||||||
sortable: true,
|
sortable: true,
|
||||||
field: 'quantity',
|
field: 'quantity',
|
||||||
title: '{% trans "Quantity" %}'
|
title: '{% trans "Quantity" %}',
|
||||||
|
footerFormatter: function(data) {
|
||||||
|
return data.map(function (row) {
|
||||||
|
return +row['quantity']
|
||||||
|
}).reduce(function (sum, i) {
|
||||||
|
return sum + i
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
sortable: true,
|
||||||
@ -182,6 +193,25 @@ $("#po-table").inventreeTable({
|
|||||||
return row.purchase_price_string || row.purchase_price;
|
return row.purchase_price_string || row.purchase_price;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
title: '{% trans "Total price" %}',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
var total = row.purchase_price * row.quantity;
|
||||||
|
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.purchase_price_currency});
|
||||||
|
return formatter.format(total)
|
||||||
|
},
|
||||||
|
footerFormatter: function(data) {
|
||||||
|
var total = data.map(function (row) {
|
||||||
|
return +row['purchase_price']*row['quantity']
|
||||||
|
}).reduce(function (sum, i) {
|
||||||
|
return sum + i
|
||||||
|
}, 0)
|
||||||
|
var currency = (data.slice(-1)[0] && data.slice(-1)[0].purchase_price_currency) || 'USD';
|
||||||
|
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: currency});
|
||||||
|
return formatter.format(total)
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
sortable: true,
|
||||||
field: 'received',
|
field: 'received',
|
||||||
@ -204,6 +234,10 @@ $("#po-table").inventreeTable({
|
|||||||
return (progressA < progressB) ? 1 : -1;
|
return (progressA < progressB) ? 1 : -1;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'destination.pathstring',
|
||||||
|
title: '{% trans "Destination" %}',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'notes',
|
field: 'notes',
|
||||||
title: '{% trans "Notes" %}',
|
title: '{% trans "Notes" %}',
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
<th>{% trans "Received" %}</th>
|
<th>{% trans "Received" %}</th>
|
||||||
<th>{% trans "Receive" %}</th>
|
<th>{% trans "Receive" %}</th>
|
||||||
<th>{% trans "Status" %}</th>
|
<th>{% trans "Status" %}</th>
|
||||||
|
<th>{% trans "Destination" %}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for line in lines %}
|
{% for line in lines %}
|
||||||
@ -53,6 +54,9 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ line.get_destination }}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button class='btn btn-default btn-remove' onClick="removeOrderRowFromOrderWizard()" id='del_item_{{ line.id }}' title='{% trans "Remove line" %}' type='button'>
|
<button class='btn btn-default btn-remove' onClick="removeOrderRowFromOrderWizard()" id='del_item_{{ line.id }}' title='{% trans "Remove line" %}' type='button'>
|
||||||
<span row='line_row_{{ line.id }}' class='fas fa-times-circle icon-red'></span>
|
<span row='line_row_{{ line.id }}' class='fas fa-times-circle icon-red'></span>
|
||||||
|
@ -199,6 +199,7 @@ $("#so-lines-table").inventreeTable({
|
|||||||
detailFormatter: showFulfilledSubTable,
|
detailFormatter: showFulfilledSubTable,
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
showFooter: true,
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
field: 'pk',
|
field: 'pk',
|
||||||
@ -217,7 +218,10 @@ $("#so-lines-table").inventreeTable({
|
|||||||
} else {
|
} else {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
footerFormatter: function() {
|
||||||
|
return '{% trans "Total" %}'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
sortable: true,
|
||||||
@ -228,6 +232,13 @@ $("#so-lines-table").inventreeTable({
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
field: 'quantity',
|
field: 'quantity',
|
||||||
title: '{% trans "Quantity" %}',
|
title: '{% trans "Quantity" %}',
|
||||||
|
footerFormatter: function(data) {
|
||||||
|
return data.map(function (row) {
|
||||||
|
return +row['quantity']
|
||||||
|
}).reduce(function (sum, i) {
|
||||||
|
return sum + i
|
||||||
|
}, 0)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
sortable: true,
|
sortable: true,
|
||||||
@ -237,6 +248,26 @@ $("#so-lines-table").inventreeTable({
|
|||||||
return row.sale_price_string || row.sale_price;
|
return row.sale_price_string || row.sale_price;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
sortable: true,
|
||||||
|
title: '{% trans "Total price" %}',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
var total = row.sale_price * row.quantity;
|
||||||
|
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.sale_price_currency});
|
||||||
|
return formatter.format(total)
|
||||||
|
},
|
||||||
|
footerFormatter: function(data) {
|
||||||
|
var total = data.map(function (row) {
|
||||||
|
return +row['sale_price']*row['quantity']
|
||||||
|
}).reduce(function (sum, i) {
|
||||||
|
return sum + i
|
||||||
|
}, 0)
|
||||||
|
var currency = (data.slice(-1)[0] && data.slice(-1)[0].sale_price_currency) || 'USD';
|
||||||
|
var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: currency});
|
||||||
|
return formatter.format(total)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'allocated',
|
field: 'allocated',
|
||||||
{% if order.status == SalesOrderStatus.PENDING %}
|
{% if order.status == SalesOrderStatus.PENDING %}
|
||||||
|
@ -87,7 +87,7 @@ class OrderTest(TestCase):
|
|||||||
order = PurchaseOrder.objects.get(pk=1)
|
order = PurchaseOrder.objects.get(pk=1)
|
||||||
|
|
||||||
self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
|
self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
|
||||||
self.assertEqual(order.lines.count(), 3)
|
self.assertEqual(order.lines.count(), 4)
|
||||||
|
|
||||||
sku = SupplierPart.objects.get(SKU='ACME-WIDGET')
|
sku = SupplierPart.objects.get(SKU='ACME-WIDGET')
|
||||||
part = sku.part
|
part = sku.part
|
||||||
@ -105,11 +105,11 @@ class OrderTest(TestCase):
|
|||||||
order.add_line_item(sku, 100)
|
order.add_line_item(sku, 100)
|
||||||
|
|
||||||
self.assertEqual(part.on_order, 100)
|
self.assertEqual(part.on_order, 100)
|
||||||
self.assertEqual(order.lines.count(), 4)
|
self.assertEqual(order.lines.count(), 5)
|
||||||
|
|
||||||
# Order the same part again (it should be merged)
|
# Order the same part again (it should be merged)
|
||||||
order.add_line_item(sku, 50)
|
order.add_line_item(sku, 50)
|
||||||
self.assertEqual(order.lines.count(), 4)
|
self.assertEqual(order.lines.count(), 5)
|
||||||
self.assertEqual(part.on_order, 150)
|
self.assertEqual(part.on_order, 150)
|
||||||
|
|
||||||
# Try to order a supplier part from the wrong supplier
|
# Try to order a supplier part from the wrong supplier
|
||||||
@ -163,7 +163,7 @@ class OrderTest(TestCase):
|
|||||||
loc = StockLocation.objects.get(id=1)
|
loc = StockLocation.objects.get(id=1)
|
||||||
|
|
||||||
# There should be two lines against this order
|
# There should be two lines against this order
|
||||||
self.assertEqual(len(order.pending_line_items()), 3)
|
self.assertEqual(len(order.pending_line_items()), 4)
|
||||||
|
|
||||||
# Should fail, as order is 'PENDING' not 'PLACED"
|
# Should fail, as order is 'PENDING' not 'PLACED"
|
||||||
self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
|
self.assertEqual(order.status, PurchaseOrderStatus.PENDING)
|
||||||
|
@ -15,7 +15,16 @@
|
|||||||
{% block details %}
|
{% block details %}
|
||||||
{% default_currency as currency %}
|
{% default_currency as currency %}
|
||||||
|
|
||||||
{% crispy form %}
|
<form method="post" class="form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-9">{{ form|crispy }}</div>
|
||||||
|
<div class="col-sm-3">
|
||||||
|
<input type="submit" value="{% trans 'Calculate' %}" class="btn btn-primary btn-block">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<hr>
|
||||||
|
|
||||||
<div class="row"><div class="col col-md-6">
|
<div class="row"><div class="col col-md-6">
|
||||||
<h4>{% trans "Pricing ranges" %}</h4>
|
<h4>{% trans "Pricing ranges" %}</h4>
|
||||||
@ -110,8 +119,8 @@
|
|||||||
|
|
||||||
{% if price_history %}
|
{% if price_history %}
|
||||||
<hr>
|
<hr>
|
||||||
<h4>{% trans 'Stock Pricing' %}<i class="fas fa-info-circle" title="Shows the prices of stock for this part
|
<h4>{% trans 'Stock Pricing' %}<i class="fas fa-info-circle" title="Shows the purchase prices of stock for this part.
|
||||||
the part single price shown is the current price for that supplier part"></i></h4>
|
The part single price is the current purchase price for that supplier part."></i></h4>
|
||||||
{% if price_history|length > 1 %}
|
{% if price_history|length > 1 %}
|
||||||
<div style="max-width: 99%; min-height: 300px">
|
<div style="max-width: 99%; min-height: 300px">
|
||||||
<canvas id="StockPriceChart"></canvas>
|
<canvas id="StockPriceChart"></canvas>
|
||||||
@ -157,7 +166,8 @@ the part single price shown is the current price for that supplier part"></i></h
|
|||||||
{% for line in price_history %}{{ line.price_diff|stringformat:".2f" }},{% endfor %}
|
{% for line in price_history %}{{ line.price_diff|stringformat:".2f" }},{% endfor %}
|
||||||
],
|
],
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
type: 'line'
|
type: 'line',
|
||||||
|
hidden: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '{% blocktrans %}Part Single Price - {{currency}}{% endblocktrans %}',
|
label: '{% blocktrans %}Part Single Price - {{currency}}{% endblocktrans %}',
|
||||||
@ -168,7 +178,8 @@ the part single price shown is the current price for that supplier part"></i></h
|
|||||||
{% for line in price_history %}{{ line.price_part|stringformat:".2f" }},{% endfor %}
|
{% for line in price_history %}{{ line.price_part|stringformat:".2f" }},{% endfor %}
|
||||||
],
|
],
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
type: 'line'
|
type: 'line',
|
||||||
|
hidden: true,
|
||||||
},
|
},
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{
|
{
|
||||||
|
@ -181,6 +181,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if part.trackable and part.getLatestSerialNumber %}
|
||||||
|
<tr><td colspan="3"></td></tr>
|
||||||
|
<tr>
|
||||||
|
<td><span class='fas fa-hashtag'></span></td>
|
||||||
|
<td>{% trans "Latest Serial Number" %}</td>
|
||||||
|
<td>{{ part.getLatestSerialNumber }}{% include "clip.html"%}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -161,6 +161,13 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
required_tests = serializers.IntegerField(source='required_test_count', read_only=True, required=False)
|
required_tests = serializers.IntegerField(source='required_test_count', read_only=True, required=False)
|
||||||
|
|
||||||
|
purchase_price = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_purchase_price(self, obj):
|
||||||
|
""" Return purchase_price (Money field) as string (includes currency) """
|
||||||
|
|
||||||
|
return str(obj.purchase_price) if obj.purchase_price else '-'
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
part_detail = kwargs.pop('part_detail', False)
|
part_detail = kwargs.pop('part_detail', False)
|
||||||
@ -215,6 +222,7 @@ class StockItemSerializer(InvenTreeModelSerializer):
|
|||||||
'tracking_items',
|
'tracking_items',
|
||||||
'uid',
|
'uid',
|
||||||
'updated',
|
'updated',
|
||||||
|
'purchase_price',
|
||||||
]
|
]
|
||||||
|
|
||||||
""" These fields are read-only in this context.
|
""" These fields are read-only in this context.
|
||||||
|
@ -159,7 +159,7 @@ function loadStockTestResultsTable(table, options) {
|
|||||||
|
|
||||||
// Set "parent" for each existing row
|
// Set "parent" for each existing row
|
||||||
tableData.forEach(function(item, idx) {
|
tableData.forEach(function(item, idx) {
|
||||||
tableData[idx].parent = options.stock_item;
|
tableData[idx].parent = parent_node;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Once the test template data are loaded, query for test results
|
// Once the test template data are loaded, query for test results
|
||||||
@ -660,6 +660,11 @@ function loadStockTable(table, options) {
|
|||||||
title: '{% trans "Last Updated" %}',
|
title: '{% trans "Last Updated" %}',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'purchase_price',
|
||||||
|
title: '{% trans "Purchase Price" %}',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'packaging',
|
field: 'packaging',
|
||||||
title: '{% trans "Packaging" %}',
|
title: '{% trans "Packaging" %}',
|
||||||
|
@ -7,6 +7,8 @@ ARG branch="master"
|
|||||||
ENV PYTHONUNBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
# InvenTree key settings
|
# InvenTree key settings
|
||||||
|
|
||||||
|
# The INVENTREE_HOME directory is where the InvenTree source repository will be located
|
||||||
ENV INVENTREE_HOME="/home/inventree"
|
ENV INVENTREE_HOME="/home/inventree"
|
||||||
|
|
||||||
# GitHub settings
|
# GitHub settings
|
||||||
@ -17,10 +19,9 @@ ENV INVENTREE_LOG_LEVEL="INFO"
|
|||||||
ENV INVENTREE_DOCKER="true"
|
ENV INVENTREE_DOCKER="true"
|
||||||
|
|
||||||
# InvenTree paths
|
# InvenTree paths
|
||||||
ENV INVENTREE_SRC_DIR="${INVENTREE_HOME}/src"
|
ENV INVENTREE_MNG_DIR="${INVENTREE_HOME}/InvenTree"
|
||||||
ENV INVENTREE_MNG_DIR="${INVENTREE_SRC_DIR}/InvenTree"
|
|
||||||
ENV INVENTREE_DATA_DIR="${INVENTREE_HOME}/data"
|
ENV INVENTREE_DATA_DIR="${INVENTREE_HOME}/data"
|
||||||
ENV INVENTREE_STATIC_ROOT="${INVENTREE_HOME}/static"
|
ENV INVENTREE_STATIC_ROOT="${INVENTREE_DATA_DIR}/static"
|
||||||
ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DATA_DIR}/media"
|
ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DATA_DIR}/media"
|
||||||
|
|
||||||
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
|
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DATA_DIR}/config.yaml"
|
||||||
@ -44,8 +45,6 @@ RUN addgroup -S inventreegroup && adduser -S inventree -G inventreegroup
|
|||||||
|
|
||||||
WORKDIR ${INVENTREE_HOME}
|
WORKDIR ${INVENTREE_HOME}
|
||||||
|
|
||||||
RUN mkdir -p ${INVENTREE_STATIC_ROOT}
|
|
||||||
|
|
||||||
# Install required system packages
|
# Install required system packages
|
||||||
RUN apk add --no-cache git make bash \
|
RUN apk add --no-cache git make bash \
|
||||||
gcc libgcc g++ libstdc++ \
|
gcc libgcc g++ libstdc++ \
|
||||||
@ -78,37 +77,40 @@ RUN pip install --no-cache-dir -U gunicorn
|
|||||||
FROM base as production
|
FROM base as production
|
||||||
# Clone source code
|
# Clone source code
|
||||||
RUN echo "Downloading InvenTree from ${INVENTREE_REPO}"
|
RUN echo "Downloading InvenTree from ${INVENTREE_REPO}"
|
||||||
RUN git clone --branch ${INVENTREE_BRANCH} --depth 1 ${INVENTREE_REPO} ${INVENTREE_SRC_DIR}
|
RUN git clone --branch ${INVENTREE_BRANCH} --depth 1 ${INVENTREE_REPO} ${INVENTREE_HOME}
|
||||||
|
|
||||||
# Install InvenTree packages
|
# Install InvenTree packages
|
||||||
RUN pip install --no-cache-dir -U -r ${INVENTREE_SRC_DIR}/requirements.txt
|
RUN pip install --no-cache-dir -U -r ${INVENTREE_HOME}/requirements.txt
|
||||||
|
|
||||||
# Copy gunicorn config file
|
# Copy gunicorn config file
|
||||||
COPY gunicorn.conf.py ${INVENTREE_HOME}/gunicorn.conf.py
|
COPY gunicorn.conf.py ${INVENTREE_HOME}/gunicorn.conf.py
|
||||||
|
|
||||||
# Copy startup scripts
|
# Copy startup scripts
|
||||||
COPY start_prod_server.sh ${INVENTREE_SRC_DIR}/start_prod_server.sh
|
COPY start_prod_server.sh ${INVENTREE_HOME}/start_prod_server.sh
|
||||||
COPY start_worker.sh ${INVENTREE_SRC_DIR}/start_worker.sh
|
COPY start_prod_worker.sh ${INVENTREE_HOME}/start_prod_worker.sh
|
||||||
|
|
||||||
RUN chmod 755 ${INVENTREE_SRC_DIR}/start_prod_server.sh
|
RUN chmod 755 ${INVENTREE_HOME}/start_prod_server.sh
|
||||||
RUN chmod 755 ${INVENTREE_SRC_DIR}/start_worker.sh
|
RUN chmod 755 ${INVENTREE_HOME}/start_prod_worker.sh
|
||||||
|
|
||||||
# exec commands should be executed from the "src" directory
|
WORKDIR ${INVENTREE_HOME}
|
||||||
WORKDIR ${INVENTREE_SRC_DIR}
|
|
||||||
|
|
||||||
# Let us begin
|
# Let us begin
|
||||||
CMD ["bash", "./start_prod_server.sh"]
|
CMD ["bash", "./start_prod_server.sh"]
|
||||||
|
|
||||||
FROM base as dev
|
FROM base as dev
|
||||||
|
|
||||||
# The development image requires the source code to be mounted to /home/inventree/src/
|
# The development image requires the source code to be mounted to /home/inventree/
|
||||||
# So from here, we don't actually "do" anything
|
# So from here, we don't actually "do" anything, apart from some file management
|
||||||
|
|
||||||
WORKDIR ${INVENTREE_SRC_DIR}
|
ENV INVENTREE_DEV_DIR = "${INVENTREE_HOME}/dev"
|
||||||
|
|
||||||
COPY start_dev_server.sh ${INVENTREE_HOME}/start_dev_server.sh
|
# Override default path settings
|
||||||
COPY start_dev_worker.sh ${INVENTREE_HOME}/start_dev_worker.sh
|
ENV INVENTREE_STATIC_ROOT="${INVENTREE_DEV_DIR}/static"
|
||||||
RUN chmod 755 ${INVENTREE_HOME}/start_dev_server.sh
|
ENV INVENTREE_MEDIA_ROOT="${INVENTREE_DEV_DIR}/media"
|
||||||
RUN chmod 755 ${INVENTREE_HOME}/start_dev_worker.sh
|
ENV INVENTREE_CONFIG_FILE="${INVENTREE_DEV_DIR}/config.yaml"
|
||||||
|
ENV INVENTREE_SECRET_KEY_FILE="${INVENTREE_DEV_DIR}/secret_key.txt"
|
||||||
|
|
||||||
CMD ["bash", "/home/inventree/start_dev_server.sh"]
|
WORKDIR ${INVENTREE_HOME}
|
||||||
|
|
||||||
|
# Launch the development server
|
||||||
|
CMD ["bash", "/home/inventree/docker/start_dev_server.sh"]
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
INVENTREE_DB_ENGINE=sqlite3
|
INVENTREE_DB_ENGINE=sqlite3
|
||||||
INVENTREE_DB_NAME=/home/inventree/src/inventree_docker_dev.sqlite3
|
INVENTREE_DB_NAME=/home/inventree/dev/inventree_db.sqlite3
|
||||||
INVENTREE_MEDIA_ROOT=/home/inventree/src/inventree_media
|
INVENTREE_MEDIA_ROOT=/home/inventree/dev/media
|
||||||
INVENTREE_STATIC_ROOT=/home/inventree/src/inventree_static
|
INVENTREE_STATIC_ROOT=/home/inventree/dev/static
|
||||||
INVENTREE_CONFIG_FILE=/home/inventree/src/config.yaml
|
INVENTREE_CONFIG_FILE=/home/inventree/dev/config.yaml
|
||||||
INVENTREE_SECRET_KEY_FILE=/home/inventree/src/secret_key.txt
|
INVENTREE_SECRET_KEY_FILE=/home/inventree/dev/secret_key.txt
|
||||||
INVENTREE_DEBUG=true
|
INVENTREE_DEBUG=true
|
||||||
|
INVENTREE_WEB_ADDR=0.0.0.0
|
||||||
|
INVENTREE_WEB_PORT=8000
|
@ -13,8 +13,8 @@ version: "3.8"
|
|||||||
services:
|
services:
|
||||||
# InvenTree web server services
|
# InvenTree web server services
|
||||||
# Uses gunicorn as the web server
|
# Uses gunicorn as the web server
|
||||||
inventree-server:
|
inventree-dev-server:
|
||||||
container_name: inventree-server
|
container_name: inventree-dev-server
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
target: dev
|
target: dev
|
||||||
@ -22,7 +22,7 @@ services:
|
|||||||
- 8000:8000
|
- 8000:8000
|
||||||
volumes:
|
volumes:
|
||||||
# Ensure you specify the location of the 'src' directory at the end of this file
|
# Ensure you specify the location of the 'src' directory at the end of this file
|
||||||
- src:/home/inventree/src
|
- src:/home/inventree
|
||||||
env_file:
|
env_file:
|
||||||
# Environment variables required for the dev server are configured in dev-config.env
|
# Environment variables required for the dev server are configured in dev-config.env
|
||||||
- dev-config.env
|
- dev-config.env
|
||||||
@ -30,24 +30,24 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# Background worker process handles long-running or periodic tasks
|
# Background worker process handles long-running or periodic tasks
|
||||||
inventree-worker:
|
inventree-dev-worker:
|
||||||
container_name: inventree-worker
|
container_name: inventree-dev-worker
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
target: dev
|
target: dev
|
||||||
entrypoint: /home/inventree/start_dev_worker.sh
|
entrypoint: /home/inventree/docker/start_dev_worker.sh
|
||||||
depends_on:
|
depends_on:
|
||||||
- inventree-server
|
- inventree-dev-server
|
||||||
volumes:
|
volumes:
|
||||||
# Ensure you specify the location of the 'src' directory at the end of this file
|
# Ensure you specify the location of the 'src' directory at the end of this file
|
||||||
- src:/home/inventree/src
|
- src:/home/inventree
|
||||||
env_file:
|
env_file:
|
||||||
# Environment variables required for the dev server are configured in dev-config.env
|
# Environment variables required for the dev server are configured in dev-config.env
|
||||||
- dev-config.env
|
- dev-config.env
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
# NOTE: Change /path/to/src to a directory on your local machine, where the InvenTree source code is located
|
# NOTE: Change "../" to a directory on your local machine, where the InvenTree source code is located
|
||||||
# Persistent data, stored external to the container(s)
|
# Persistent data, stored external to the container(s)
|
||||||
src:
|
src:
|
||||||
driver: local
|
driver: local
|
||||||
@ -55,5 +55,5 @@ volumes:
|
|||||||
type: none
|
type: none
|
||||||
o: bind
|
o: bind
|
||||||
# This directory specified where InvenTree source code is stored "outside" the docker containers
|
# This directory specified where InvenTree source code is stored "outside" the docker containers
|
||||||
# Note: This directory must conatin the file *manage.py*
|
# By default, this directory is one level above the "docker" directory
|
||||||
device: /path/to/inventree/src
|
device: ../
|
||||||
|
@ -30,6 +30,7 @@ services:
|
|||||||
- POSTGRES_USER=pguser
|
- POSTGRES_USER=pguser
|
||||||
- POSTGRES_PASSWORD=pgpassword
|
- POSTGRES_PASSWORD=pgpassword
|
||||||
volumes:
|
volumes:
|
||||||
|
# Map 'data' volume such that postgres database is stored externally
|
||||||
- data:/var/lib/postgresql/data/
|
- data:/var/lib/postgresql/data/
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
@ -43,8 +44,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- inventree-db
|
- inventree-db
|
||||||
volumes:
|
volumes:
|
||||||
|
# Data volume must map to /home/inventree/data
|
||||||
- data:/home/inventree/data
|
- data:/home/inventree/data
|
||||||
- static:/home/inventree/static
|
|
||||||
environment:
|
environment:
|
||||||
# Default environment variables are configured to match the 'db' container
|
# Default environment variables are configured to match the 'db' container
|
||||||
# Note: If you change the database image, these will need to be adjusted
|
# Note: If you change the database image, these will need to be adjusted
|
||||||
@ -61,13 +62,13 @@ services:
|
|||||||
inventree-worker:
|
inventree-worker:
|
||||||
container_name: inventree-worker
|
container_name: inventree-worker
|
||||||
image: inventree/inventree:latest
|
image: inventree/inventree:latest
|
||||||
entrypoint: ./start_worker.sh
|
entrypoint: ./start_prod_worker.sh
|
||||||
depends_on:
|
depends_on:
|
||||||
- inventree-db
|
- inventree-db
|
||||||
- inventree-server
|
- inventree-server
|
||||||
volumes:
|
volumes:
|
||||||
|
# Data volume must map to /home/inventree/data
|
||||||
- data:/home/inventree/data
|
- data:/home/inventree/data
|
||||||
- static:/home/inventree/static
|
|
||||||
environment:
|
environment:
|
||||||
# Default environment variables are configured to match the 'db' container
|
# Default environment variables are configured to match the 'db' container
|
||||||
# Note: If you change the database image, these will need to be adjusted
|
# Note: If you change the database image, these will need to be adjusted
|
||||||
@ -81,7 +82,8 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# nginx acts as a reverse proxy
|
# nginx acts as a reverse proxy
|
||||||
# static files are served by nginx
|
# static files are served directly by nginx
|
||||||
|
# media files are served by nginx, although authentication is redirected to inventree-server
|
||||||
# web requests are redirected to gunicorn
|
# web requests are redirected to gunicorn
|
||||||
# NOTE: You will need to provide a working nginx.conf file!
|
# NOTE: You will need to provide a working nginx.conf file!
|
||||||
inventree-proxy:
|
inventree-proxy:
|
||||||
@ -93,11 +95,11 @@ services:
|
|||||||
# Change "1337" to the port that you want InvenTree web server to be available on
|
# Change "1337" to the port that you want InvenTree web server to be available on
|
||||||
- 1337:80
|
- 1337:80
|
||||||
volumes:
|
volumes:
|
||||||
# Provide nginx.conf file to the container
|
# Provide ./nginx.conf file to the container
|
||||||
# Refer to the provided example file as a starting point
|
# Refer to the provided example file as a starting point
|
||||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
# Static data volume is mounted to /var/www/static
|
# nginx proxy needs access to static and media files
|
||||||
- static:/var/www/static:ro
|
- data:/var/www
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
@ -111,5 +113,3 @@ volumes:
|
|||||||
# This directory specified where InvenTree data are stored "outside" the docker containers
|
# This directory specified where InvenTree data are stored "outside" the docker containers
|
||||||
# Change this path to a local system path where you want InvenTree data stored
|
# Change this path to a local system path where you want InvenTree data stored
|
||||||
device: /path/to/data
|
device: /path/to/data
|
||||||
# Static files, shared between containers
|
|
||||||
static:
|
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
server {
|
server {
|
||||||
|
|
||||||
# Listen for connection on (internal) port 80
|
# Listen for connection on (internal) port 80
|
||||||
@ -34,4 +35,23 @@ server {
|
|||||||
add_header Cache-Control "public";
|
add_header Cache-Control "public";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Redirect any requests for media files
|
||||||
|
location /media/ {
|
||||||
|
alias /var/www/media/;
|
||||||
|
|
||||||
|
# Media files require user authentication
|
||||||
|
auth_request /auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use the 'user' API endpoint for auth
|
||||||
|
location /auth {
|
||||||
|
internal;
|
||||||
|
|
||||||
|
proxy_pass http://inventree-server:8000/auth/;
|
||||||
|
|
||||||
|
proxy_pass_request_body off;
|
||||||
|
proxy_set_header Content-Length "";
|
||||||
|
proxy_set_header X-Original-URI $request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -16,21 +16,22 @@ if test -f "$INVENTREE_CONFIG_FILE"; then
|
|||||||
echo "$INVENTREE_CONFIG_FILE exists - skipping"
|
echo "$INVENTREE_CONFIG_FILE exists - skipping"
|
||||||
else
|
else
|
||||||
echo "Copying config file to $INVENTREE_CONFIG_FILE"
|
echo "Copying config file to $INVENTREE_CONFIG_FILE"
|
||||||
cp $INVENTREE_SRC_DIR/InvenTree/config_template.yaml $INVENTREE_CONFIG_FILE
|
cp $INVENTREE_HOME/InvenTree/config_template.yaml $INVENTREE_CONFIG_FILE
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Setup a virtual environment
|
# Setup a virtual environment (within the "dev" directory)
|
||||||
python3 -m venv inventree-docker-dev
|
python3 -m venv ./dev/env
|
||||||
|
|
||||||
source inventree-docker-dev/bin/activate
|
# Activate the virtual environment
|
||||||
|
source ./dev/env/bin/activate
|
||||||
|
|
||||||
echo "Installing required packages..."
|
echo "Installing required packages..."
|
||||||
pip install --no-cache-dir -U -r ${INVENTREE_SRC_DIR}/requirements.txt
|
pip install --no-cache-dir -U -r ${INVENTREE_HOME}/requirements.txt
|
||||||
|
|
||||||
echo "Starting InvenTree server..."
|
echo "Starting InvenTree server..."
|
||||||
|
|
||||||
# Wait for the database to be ready
|
# Wait for the database to be ready
|
||||||
cd $INVENTREE_MNG_DIR
|
cd ${INVENTREE_HOME}/InvenTree
|
||||||
python manage.py wait_for_db
|
python manage.py wait_for_db
|
||||||
|
|
||||||
sleep 10
|
sleep 10
|
||||||
@ -45,4 +46,4 @@ python manage.py migrate --run-syncdb || exit 1
|
|||||||
python manage.py clearsessions || exit 1
|
python manage.py clearsessions || exit 1
|
||||||
|
|
||||||
# Launch a development server
|
# Launch a development server
|
||||||
python manage.py runserver 0.0.0.0:$INVENTREE_WEB_PORT
|
python manage.py runserver ${INVENTREE_WEB_ADDR}:${INVENTREE_WEB_PORT}
|
||||||
|
@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
echo "Starting InvenTree worker..."
|
echo "Starting InvenTree worker..."
|
||||||
|
|
||||||
cd $INVENTREE_SRC_DIR
|
cd $INVENTREE_HOME
|
||||||
|
|
||||||
# Activate virtual environment
|
# Activate virtual environment
|
||||||
source inventree-docker-dev/bin/activate
|
source ./dev/env/bin/activate
|
||||||
|
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
||||||
# Wait for the database to be ready
|
# Wait for the database to be ready
|
||||||
cd $INVENTREE_MNG_DIR
|
cd InvenTree
|
||||||
python manage.py wait_for_db
|
python manage.py wait_for_db
|
||||||
|
|
||||||
sleep 10
|
sleep 10
|
||||||
|
@ -16,7 +16,7 @@ if test -f "$INVENTREE_CONFIG_FILE"; then
|
|||||||
echo "$INVENTREE_CONFIG_FILE exists - skipping"
|
echo "$INVENTREE_CONFIG_FILE exists - skipping"
|
||||||
else
|
else
|
||||||
echo "Copying config file to $INVENTREE_CONFIG_FILE"
|
echo "Copying config file to $INVENTREE_CONFIG_FILE"
|
||||||
cp $INVENTREE_SRC_DIR/InvenTree/config_template.yaml $INVENTREE_CONFIG_FILE
|
cp $INVENTREE_HOME/InvenTree/config_template.yaml $INVENTREE_CONFIG_FILE
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Starting InvenTree server..."
|
echo "Starting InvenTree server..."
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
invoke>=1.4.0 # Invoke build tool
|
invoke>=1.4.0 # Invoke build tool
|
||||||
wheel>=0.34.2 # Wheel
|
wheel>=0.34.2 # Wheel
|
||||||
Django==3.2.1 # Django package
|
Django==3.2.4 # Django package
|
||||||
pillow==8.1.1 # Image manipulation
|
pillow==8.2.0 # Image manipulation
|
||||||
djangorestframework==3.12.4 # DRF framework
|
djangorestframework==3.12.4 # DRF framework
|
||||||
django-cors-headers==3.2.0 # CORS headers extension for DRF
|
django-cors-headers==3.2.0 # CORS headers extension for DRF
|
||||||
django-filter==2.4.0 # Extended filtering options
|
django-filter==2.4.0 # Extended filtering options
|
||||||
|
4
tasks.py
4
tasks.py
@ -282,7 +282,7 @@ def export_records(c, filename='data.json'):
|
|||||||
|
|
||||||
tmpfile = f"{filename}.tmp"
|
tmpfile = f"{filename}.tmp"
|
||||||
|
|
||||||
cmd = f"dumpdata --indent 2 --output {tmpfile} {content_excludes()}"
|
cmd = f"dumpdata --indent 2 --output '{tmpfile}' {content_excludes()}"
|
||||||
|
|
||||||
# Dump data to temporary file
|
# Dump data to temporary file
|
||||||
manage(c, cmd, pty=True)
|
manage(c, cmd, pty=True)
|
||||||
@ -348,7 +348,7 @@ def import_records(c, filename='data.json'):
|
|||||||
with open(tmpfile, "w") as f_out:
|
with open(tmpfile, "w") as f_out:
|
||||||
f_out.write(json.dumps(data, indent=2))
|
f_out.write(json.dumps(data, indent=2))
|
||||||
|
|
||||||
cmd = f"loaddata {tmpfile} -i {content_excludes()}"
|
cmd = f"loaddata '{tmpfile}' -i {content_excludes()}"
|
||||||
|
|
||||||
manage(c, cmd, pty=True)
|
manage(c, cmd, pty=True)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user