mirror of
https://github.com/inventree/InvenTree.git
synced 2026-10-12 13:11:41 +00:00
Merge branch 'inventree:master' into matmair/issue2279
This commit is contained in:
49 files changed
+24984
-23617
No files matched your search
@@ -4,8 +4,13 @@ import logging
|
|||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.core.exceptions import AppRegistryNotReady
|
from django.core.exceptions import AppRegistryNotReady
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import transaction
|
||||||
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
from InvenTree.ready import isInTestMode, canAppAccessDatabase
|
from InvenTree.ready import isInTestMode, canAppAccessDatabase
|
||||||
|
from .config import get_setting
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
|
|
||||||
|
|
||||||
@@ -26,6 +31,9 @@ class InvenTreeConfig(AppConfig):
|
|||||||
if not isInTestMode():
|
if not isInTestMode():
|
||||||
self.update_exchange_rates()
|
self.update_exchange_rates()
|
||||||
|
|
||||||
|
if canAppAccessDatabase() or settings.TESTING_ENV:
|
||||||
|
self.add_user_on_startup()
|
||||||
|
|
||||||
def remove_obsolete_tasks(self):
|
def remove_obsolete_tasks(self):
|
||||||
"""
|
"""
|
||||||
Delete any obsolete scheduled tasks in the database
|
Delete any obsolete scheduled tasks in the database
|
||||||
@@ -138,3 +146,54 @@ class InvenTreeConfig(AppConfig):
|
|||||||
update_exchange_rates()
|
update_exchange_rates()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error updating exchange rates: {e}")
|
logger.error(f"Error updating exchange rates: {e}")
|
||||||
|
|
||||||
|
def add_user_on_startup(self):
|
||||||
|
"""Add a user on startup"""
|
||||||
|
# stop if checks were already created
|
||||||
|
if hasattr(settings, 'USER_ADDED') and settings.USER_ADDED:
|
||||||
|
return
|
||||||
|
|
||||||
|
# get values
|
||||||
|
add_user = get_setting(
|
||||||
|
'INVENTREE_ADMIN_USER',
|
||||||
|
settings.CONFIG.get('admin_user', False)
|
||||||
|
)
|
||||||
|
add_email = get_setting(
|
||||||
|
'INVENTREE_ADMIN_EMAIL',
|
||||||
|
settings.CONFIG.get('admin_email', False)
|
||||||
|
)
|
||||||
|
add_password = get_setting(
|
||||||
|
'INVENTREE_ADMIN_PASSWORD',
|
||||||
|
settings.CONFIG.get('admin_password', False)
|
||||||
|
)
|
||||||
|
|
||||||
|
# check if all values are present
|
||||||
|
set_variables = 0
|
||||||
|
for tested_var in [add_user, add_email, add_password]:
|
||||||
|
if tested_var:
|
||||||
|
set_variables += 1
|
||||||
|
|
||||||
|
# no variable set -> do not try anything
|
||||||
|
if set_variables == 0:
|
||||||
|
settings.USER_ADDED = True
|
||||||
|
return
|
||||||
|
|
||||||
|
# not all needed variables set
|
||||||
|
if set_variables < 3:
|
||||||
|
logger.warn('Not all required settings for adding a user on startup are present:\nINVENTREE_SET_USER, INVENTREE_SET_EMAIL, INVENTREE_SET_PASSWORD')
|
||||||
|
settings.USER_ADDED = True
|
||||||
|
return
|
||||||
|
|
||||||
|
# good to go -> create user
|
||||||
|
user = get_user_model()
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
|
new_user = user.objects.create_superuser(add_user, add_email, add_password)
|
||||||
|
logger.info(f'User {str(new_user)} was created!')
|
||||||
|
except IntegrityError as _e:
|
||||||
|
logger.warning(f'The user "{add_user}" could not be created due to the following error:\n{str(_e)}')
|
||||||
|
if settings.TESTING_ENV:
|
||||||
|
raise _e
|
||||||
|
|
||||||
|
# do not try again
|
||||||
|
settings.USER_ADDED = True
|
||||||
@@ -37,6 +37,8 @@ def _is_true(x):
|
|||||||
|
|
||||||
# Determine if we are running in "test" mode e.g. "manage.py test"
|
# Determine if we are running in "test" mode e.g. "manage.py test"
|
||||||
TESTING = 'test' in sys.argv
|
TESTING = 'test' in sys.argv
|
||||||
|
# Are enviroment variables manipulated by tests? Needs to be set by testing code
|
||||||
|
TESTING_ENV = False
|
||||||
|
|
||||||
# New requirement for django 3.2+
|
# New requirement for django 3.2+
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
|
from test.support import EnvironmentVarGuard
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase, override_settings
|
||||||
import django.core.exceptions as django_exceptions
|
import django.core.exceptions as django_exceptions
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from djmoney.money import Money
|
from djmoney.money import Money
|
||||||
from djmoney.contrib.exchange.models import Rate, convert_money
|
from djmoney.contrib.exchange.models import Rate, convert_money
|
||||||
@@ -407,3 +410,46 @@ class TestStatus(TestCase):
|
|||||||
|
|
||||||
def test_Importing(self):
|
def test_Importing(self):
|
||||||
self.assertEqual(ready.isImportingData(), False)
|
self.assertEqual(ready.isImportingData(), False)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSettings(TestCase):
|
||||||
|
"""
|
||||||
|
Unit tests for settings
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.user_mdl = get_user_model()
|
||||||
|
self.env = EnvironmentVarGuard()
|
||||||
|
|
||||||
|
def run_reload(self):
|
||||||
|
from plugin import registry
|
||||||
|
|
||||||
|
with self.env:
|
||||||
|
settings.USER_ADDED = False
|
||||||
|
registry.reload_plugins()
|
||||||
|
|
||||||
|
@override_settings(TESTING_ENV=True)
|
||||||
|
def test_set_user_to_few(self):
|
||||||
|
# add shortcut
|
||||||
|
user_count = self.user_mdl.objects.count
|
||||||
|
# enable testing mode
|
||||||
|
settings.TESTING_ENV = True
|
||||||
|
|
||||||
|
# nothing set
|
||||||
|
self.run_reload()
|
||||||
|
self.assertEqual(user_count(), 0)
|
||||||
|
|
||||||
|
# not enough set
|
||||||
|
self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username
|
||||||
|
self.run_reload()
|
||||||
|
self.assertEqual(user_count(), 0)
|
||||||
|
|
||||||
|
# enough set
|
||||||
|
self.env.set('INVENTREE_ADMIN_USER', 'admin') # set username
|
||||||
|
self.env.set('INVENTREE_ADMIN_EMAIL', 'info@example.com') # set email
|
||||||
|
self.env.set('INVENTREE_ADMIN_PASSWORD', 'password123') # set password
|
||||||
|
self.run_reload()
|
||||||
|
self.assertEqual(user_count(), 1)
|
||||||
|
|
||||||
|
# make sure to clean up
|
||||||
|
settings.TESTING_ENV = False
|
||||||
@@ -169,7 +169,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
<div id='assigned-stock-button-toolbar'>
|
<div id='assigned-stock-button-toolbar'>
|
||||||
{% include "filter_list.html" with id="customerstock" %}
|
<div class='btn-group' role='group'>
|
||||||
|
{% include "filter_list.html" with id="customerstock" %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class='table table-striped table-condensed' id='assigned-stock-table' data-toolbar='#assigned-stock-button-toolbar'></table>
|
<table class='table table-striped table-condensed' id='assigned-stock-table' data-toolbar='#assigned-stock-button-toolbar'></table>
|
||||||
@@ -282,12 +284,6 @@
|
|||||||
filterKey: "companystock",
|
filterKey: "companystock",
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#stock-export").click(function() {
|
|
||||||
exportStock({
|
|
||||||
supplier: {{ company.id }}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
{% if company.is_manufacturer %}
|
{% if company.is_manufacturer %}
|
||||||
|
|
||||||
function reloadManufacturerPartTable() {
|
function reloadManufacturerPartTable() {
|
||||||
|
|||||||
@@ -308,14 +308,6 @@ loadStockTable($("#stock-table"), {
|
|||||||
url: "{% url 'api-stock-list' %}",
|
url: "{% url 'api-stock-list' %}",
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#stock-export").click(function() {
|
|
||||||
|
|
||||||
exportStock({
|
|
||||||
supplier_part: {{ part.pk }},
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#item-create").click(function() {
|
$("#item-create").click(function() {
|
||||||
createNewStockItem({
|
createNewStockItem({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -154,6 +154,11 @@ static_root: '/home/inventree/data/static'
|
|||||||
# Use environment variable INVENTREE_LOGIN_ATTEMPTS
|
# Use environment variable INVENTREE_LOGIN_ATTEMPTS
|
||||||
#login_attempts: 5
|
#login_attempts: 5
|
||||||
|
|
||||||
|
# Add new user on first startup
|
||||||
|
#admin_user: admin
|
||||||
|
#admin_email: info@example.com
|
||||||
|
#admin_password: inventree
|
||||||
|
|
||||||
# Permit custom authentication backends
|
# Permit custom authentication backends
|
||||||
#authentication_backends:
|
#authentication_backends:
|
||||||
# - 'django.contrib.auth.backends.ModelBackend'
|
# - 'django.contrib.auth.backends.ModelBackend'
|
||||||
|
|||||||
+1148
-1088
File diff suppressed because it is too large.
Load diff
+1145
-1085
File diff suppressed because it is too large.
Load diff
+1148
-1088
File diff suppressed because it is too large.
Load diff
+1146
-1086
File diff suppressed because it is too large.
Load diff
+1145
-1085
File diff suppressed because it is too large.
Load diff
+2680
-2620
File diff suppressed because it is too large.
Load diff
+1145
-1085
File diff suppressed because it is too large.
Load diff
+1156
-1096
File diff suppressed because it is too large.
Load diff
+1145
-1085
File diff suppressed because it is too large.
Load diff
+1145
-1085
File diff suppressed because it is too large.
Load diff
+1146
-1086
File diff suppressed because it is too large.
Load diff
+1146
-1086
File diff suppressed because it is too large.
Load diff
+1147
-1087
File diff suppressed because it is too large.
Load diff
+1145
-1085
File diff suppressed because it is too large.
Load diff
+1146
-1086
File diff suppressed because it is too large.
Load diff
+1145
-1085
File diff suppressed because it is too large.
Load diff
+1145
-1085
File diff suppressed because it is too large.
Load diff
+1146
-1086
File diff suppressed because it is too large.
Load diff
+1145
-1085
File diff suppressed because it is too large.
Load diff
+1146
-1086
File diff suppressed because it is too large.
Load diff
+30
-1
@@ -16,10 +16,11 @@ from rest_framework.response import Response
|
|||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
|
|
||||||
from InvenTree.filters import InvenTreeOrderingFilter
|
from InvenTree.filters import InvenTreeOrderingFilter
|
||||||
from InvenTree.helpers import str2bool
|
from InvenTree.helpers import str2bool, DownloadFile
|
||||||
from InvenTree.api import AttachmentMixin
|
from InvenTree.api import AttachmentMixin
|
||||||
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
|
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
|
||||||
|
|
||||||
|
from order.admin import POLineItemResource
|
||||||
import order.models as models
|
import order.models as models
|
||||||
import order.serializers as serializers
|
import order.serializers as serializers
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
@@ -370,6 +371,34 @@ class POLineItemList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
|
||||||
|
# Check if we wish to export the queried data to a file
|
||||||
|
export_format = request.query_params.get('export', None)
|
||||||
|
|
||||||
|
if export_format:
|
||||||
|
export_format = str(export_format).strip().lower()
|
||||||
|
|
||||||
|
if export_format in ['csv', 'tsv', 'xls', 'xlsx']:
|
||||||
|
dataset = POLineItemResource().export(queryset=queryset)
|
||||||
|
|
||||||
|
filedata = dataset.export(export_format)
|
||||||
|
|
||||||
|
filename = f"InvenTree_PurchaseOrderData.{export_format}"
|
||||||
|
|
||||||
|
return DownloadFile(filedata, filename)
|
||||||
|
|
||||||
|
page = self.paginate_queryset(queryset)
|
||||||
|
|
||||||
|
if page is not None:
|
||||||
|
serializer = self.get_serializer(page, many=True)
|
||||||
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
filter_backends = [
|
filter_backends = [
|
||||||
rest_filters.DjangoFilterBackend,
|
rest_filters.DjangoFilterBackend,
|
||||||
filters.SearchFilter,
|
filters.SearchFilter,
|
||||||
|
|||||||
@@ -152,32 +152,16 @@
|
|||||||
{% if order.status == PurchaseOrderStatus.PENDING %}
|
{% if order.status == PurchaseOrderStatus.PENDING %}
|
||||||
$('#new-po-line').click(function() {
|
$('#new-po-line').click(function() {
|
||||||
|
|
||||||
|
var fields = poLineItemFields({
|
||||||
|
order: {{ order.pk }},
|
||||||
|
supplier: {{ order.supplier.pk }},
|
||||||
|
{% if order.supplier.currency %}
|
||||||
|
currency: '{{ order.supplier.currency }}',
|
||||||
|
{% endif %}
|
||||||
|
});
|
||||||
|
|
||||||
constructForm('{% url "api-po-line-list" %}', {
|
constructForm('{% url "api-po-line-list" %}', {
|
||||||
fields: {
|
fields: fields,
|
||||||
order: {
|
|
||||||
value: {{ order.pk }},
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
part: {
|
|
||||||
filters: {
|
|
||||||
part_detail: true,
|
|
||||||
supplier_detail: true,
|
|
||||||
supplier: {{ order.supplier.pk }},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
quantity: {},
|
|
||||||
reference: {},
|
|
||||||
purchase_price: {},
|
|
||||||
purchase_price_currency: {
|
|
||||||
{% if order.supplier.currency %}
|
|
||||||
value: '{{ order.supplier.currency }}',
|
|
||||||
{% endif %}
|
|
||||||
},
|
|
||||||
target_date: {},
|
|
||||||
destination: {},
|
|
||||||
notes: {},
|
|
||||||
},
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
title: '{% trans "Add Line Item" %}',
|
title: '{% trans "Add Line Item" %}',
|
||||||
onSuccess: function() {
|
onSuccess: function() {
|
||||||
|
|||||||
@@ -221,29 +221,19 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function reloadTable() {
|
|
||||||
$("#so-lines-table").bootstrapTable("refresh");
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#new-so-line").click(function() {
|
$("#new-so-line").click(function() {
|
||||||
|
|
||||||
|
var fields = soLineItemFields({
|
||||||
|
order: {{ order.pk }},
|
||||||
|
});
|
||||||
|
|
||||||
constructForm('{% url "api-so-line-list" %}', {
|
constructForm('{% url "api-so-line-list" %}', {
|
||||||
fields: {
|
fields: fields,
|
||||||
order: {
|
|
||||||
value: {{ order.pk }},
|
|
||||||
hidden: true,
|
|
||||||
},
|
|
||||||
part: {},
|
|
||||||
quantity: {},
|
|
||||||
reference: {},
|
|
||||||
sale_price: {},
|
|
||||||
sale_price_currency: {},
|
|
||||||
target_date: {},
|
|
||||||
notes: {},
|
|
||||||
},
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
title: '{% trans "Add Line Item" %}',
|
title: '{% trans "Add Line Item" %}',
|
||||||
onSuccess: reloadTable,
|
onSuccess: function() {
|
||||||
|
$("#so-lines-table").bootstrapTable("refresh");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ from djmoney.contrib.exchange.exceptions import MissingRate
|
|||||||
|
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
|
|
||||||
|
from part.admin import PartResource
|
||||||
|
|
||||||
from .models import Part, PartCategory, PartRelated
|
from .models import Part, PartCategory, PartRelated
|
||||||
from .models import BomItem, BomItemSubstitute
|
from .models import BomItem, BomItemSubstitute
|
||||||
from .models import PartParameter, PartParameterTemplate
|
from .models import PartParameter, PartParameterTemplate
|
||||||
@@ -43,6 +45,7 @@ from build.models import Build
|
|||||||
from . import serializers as part_serializers
|
from . import serializers as part_serializers
|
||||||
|
|
||||||
from InvenTree.helpers import str2bool, isNull, increment
|
from InvenTree.helpers import str2bool, isNull, increment
|
||||||
|
from InvenTree.helpers import DownloadFile
|
||||||
from InvenTree.api import AttachmentMixin
|
from InvenTree.api import AttachmentMixin
|
||||||
|
|
||||||
from InvenTree.status_codes import BuildStatus
|
from InvenTree.status_codes import BuildStatus
|
||||||
@@ -726,6 +729,22 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
|
||||||
|
# Check if we wish to export the queried data to a file.
|
||||||
|
# If so, skip pagination!
|
||||||
|
export_format = request.query_params.get('export', None)
|
||||||
|
|
||||||
|
if export_format:
|
||||||
|
export_format = str(export_format).strip().lower()
|
||||||
|
|
||||||
|
if export_format in ['csv', 'tsv', 'xls', 'xlsx']:
|
||||||
|
dataset = PartResource().export(queryset=queryset)
|
||||||
|
|
||||||
|
filedata = dataset.export(export_format)
|
||||||
|
|
||||||
|
filename = f"InvenTree_Parts.{export_format}"
|
||||||
|
|
||||||
|
return DownloadFile(filedata, filename)
|
||||||
|
|
||||||
page = self.paginate_queryset(queryset)
|
page = self.paginate_queryset(queryset)
|
||||||
|
|
||||||
if page is not None:
|
if page is not None:
|
||||||
|
|||||||
@@ -1908,6 +1908,9 @@ class Part(MPTTModel):
|
|||||||
|
|
||||||
include_inherited = kwargs.get('include_inherited', False)
|
include_inherited = kwargs.get('include_inherited', False)
|
||||||
|
|
||||||
|
# Should substitute parts be duplicated?
|
||||||
|
copy_substitutes = kwargs.get('copy_substitutes', True)
|
||||||
|
|
||||||
# Copy existing BOM items from another part
|
# Copy existing BOM items from another part
|
||||||
# Note: Inherited BOM Items will *not* be duplicated!!
|
# Note: Inherited BOM Items will *not* be duplicated!!
|
||||||
for bom_item in other.get_bom_items(include_inherited=include_inherited).all():
|
for bom_item in other.get_bom_items(include_inherited=include_inherited).all():
|
||||||
@@ -1930,11 +1933,22 @@ class Part(MPTTModel):
|
|||||||
if not bom_item.sub_part.check_add_to_bom(self, raise_error=raise_error):
|
if not bom_item.sub_part.check_add_to_bom(self, raise_error=raise_error):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Obtain a list of direct substitute parts against this BomItem
|
||||||
|
substitutes = BomItemSubstitute.objects.filter(bom_item=bom_item)
|
||||||
|
|
||||||
# Construct a new BOM item
|
# Construct a new BOM item
|
||||||
bom_item.part = self
|
bom_item.part = self
|
||||||
bom_item.pk = None
|
bom_item.pk = None
|
||||||
|
|
||||||
bom_item.save()
|
bom_item.save()
|
||||||
|
bom_item.refresh_from_db()
|
||||||
|
|
||||||
|
if copy_substitutes:
|
||||||
|
for sub in substitutes:
|
||||||
|
# Duplicate the substitute (and point to the *new* BomItem object)
|
||||||
|
sub.pk = None
|
||||||
|
sub.bom_item = bom_item
|
||||||
|
sub.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def copy_parameters_from(self, other, **kwargs):
|
def copy_parameters_from(self, other, **kwargs):
|
||||||
|
|||||||
@@ -656,6 +656,9 @@ class PartCopyBOMSerializer(serializers.Serializer):
|
|||||||
fields = [
|
fields = [
|
||||||
'part',
|
'part',
|
||||||
'remove_existing',
|
'remove_existing',
|
||||||
|
'copy_substitutes',
|
||||||
|
'include_inherited',
|
||||||
|
'skip_invalid',
|
||||||
]
|
]
|
||||||
|
|
||||||
part = serializers.PrimaryKeyRelatedField(
|
part = serializers.PrimaryKeyRelatedField(
|
||||||
@@ -692,6 +695,12 @@ class PartCopyBOMSerializer(serializers.Serializer):
|
|||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
copy_substitutes = serializers.BooleanField(
|
||||||
|
label=_('Copy Substitute Parts'),
|
||||||
|
help_text=_('Copy substitute parts when duplicate BOM items'),
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""
|
"""
|
||||||
Actually duplicate the BOM
|
Actually duplicate the BOM
|
||||||
@@ -706,6 +715,7 @@ class PartCopyBOMSerializer(serializers.Serializer):
|
|||||||
clear=data.get('remove_existing', True),
|
clear=data.get('remove_existing', True),
|
||||||
skip_invalid=data.get('skip_invalid', False),
|
skip_invalid=data.get('skip_invalid', False),
|
||||||
include_inherited=data.get('include_inherited', False),
|
include_inherited=data.get('include_inherited', False),
|
||||||
|
copy_substitutes=data.get('copy_substitutes', True),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -153,9 +153,6 @@
|
|||||||
<h4>{% trans "Parts" %}</h4>
|
<h4>{% trans "Parts" %}</h4>
|
||||||
{% include "spacer.html" %}
|
{% include "spacer.html" %}
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
<button type='button' class='btn btn-outline-secondary' id='part-export' title='{% trans "Export Part Data" %}'>
|
|
||||||
<span class='fas fa-file-download'></span> {% trans "Export" %}
|
|
||||||
</button>
|
|
||||||
{% if roles.part.add %}
|
{% if roles.part.add %}
|
||||||
<button type='button' class='btn btn-success' id='part-create' title='{% trans "Create new part" %}'>
|
<button type='button' class='btn btn-success' id='part-create' title='{% trans "Create new part" %}'>
|
||||||
<span class='fas fa-plus-circle'></span> {% trans "New Part" %}
|
<span class='fas fa-plus-circle'></span> {% trans "New Part" %}
|
||||||
@@ -290,13 +287,6 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#part-export").click(function() {
|
|
||||||
|
|
||||||
var url = "{% url 'part-export' %}?category={{ category.id }}";
|
|
||||||
|
|
||||||
location.href = url;
|
|
||||||
});
|
|
||||||
|
|
||||||
{% if roles.part.add %}
|
{% if roles.part.add %}
|
||||||
$("#part-create").click(function() {
|
$("#part-create").click(function() {
|
||||||
|
|
||||||
|
|||||||
@@ -28,11 +28,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='panel-content'>
|
<div class='panel-content'>
|
||||||
{% if part.is_template %}
|
|
||||||
<div class='alert alert-info alert-block'>
|
|
||||||
{% blocktrans with full_name=part.full_name%}Showing stock for all variants of <em>{{full_name}}</em>{% endblocktrans %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% include "stock_table.html" %}
|
{% include "stock_table.html" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -281,9 +276,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul class='dropdown-menu' role='menu'>
|
<ul class='dropdown-menu' role='menu'>
|
||||||
<li><a class='dropdown-item' href='#' id='bom-upload'><span class='fas fa-file-upload'></span> {% trans "Upload BOM" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='bom-upload'><span class='fas fa-file-upload'></span> {% trans "Upload BOM" %}</a></li>
|
||||||
{% if part.variant_of %}
|
|
||||||
<li><a class='dropdown-item' href='#' id='bom-duplicate'><span class='fas fa-clone'></span> {% trans "Copy BOM" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='bom-duplicate'><span class='fas fa-clone'></span> {% trans "Copy BOM" %}</a></li>
|
||||||
{% endif %}
|
|
||||||
<li><a class='dropdown-item' href='#' id='validate-bom'><span class='fas fa-clipboard-check icon-green'></span> {% trans "Validate BOM" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='validate-bom'><span class='fas fa-clipboard-check icon-green'></span> {% trans "Validate BOM" %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -832,13 +825,6 @@
|
|||||||
url: "{% url 'api-stock-list' %}",
|
url: "{% url 'api-stock-list' %}",
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#stock-export").click(function() {
|
|
||||||
|
|
||||||
exportStock({
|
|
||||||
part: {{ part.pk }}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#item-create').click(function () {
|
$('#item-create').click(function () {
|
||||||
createNewStockItem({
|
createNewStockItem({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -58,14 +58,6 @@ class PartListTest(PartViewTestCase):
|
|||||||
self.assertIn('parts', keys)
|
self.assertIn('parts', keys)
|
||||||
self.assertIn('user', keys)
|
self.assertIn('user', keys)
|
||||||
|
|
||||||
def test_export(self):
|
|
||||||
""" Export part data to CSV """
|
|
||||||
|
|
||||||
response = self.client.get(reverse('part-export'), {'parts': '1,2,3,4,5,6,7,8,9,10'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertIn('streaming_content', dir(response))
|
|
||||||
|
|
||||||
|
|
||||||
class PartDetailTest(PartViewTestCase):
|
class PartDetailTest(PartViewTestCase):
|
||||||
|
|
||||||
|
|||||||
@@ -80,9 +80,6 @@ part_urls = [
|
|||||||
# Download a BOM upload template
|
# Download a BOM upload template
|
||||||
url(r'^bom_template/?', views.BomUploadTemplate.as_view(), name='bom-upload-template'),
|
url(r'^bom_template/?', views.BomUploadTemplate.as_view(), name='bom-upload-template'),
|
||||||
|
|
||||||
# Export data for multiple parts
|
|
||||||
url(r'^export/', views.PartExport.as_view(), name='part-export'),
|
|
||||||
|
|
||||||
# Individual part using pk
|
# Individual part using pk
|
||||||
url(r'^(?P<pk>\d+)/', include(part_detail_urls)),
|
url(r'^(?P<pk>\d+)/', include(part_detail_urls)),
|
||||||
|
|
||||||
|
|||||||
+1
-66
@@ -49,13 +49,11 @@ from . import settings as part_settings
|
|||||||
from .bom import MakeBomTemplate, ExportBom, IsValidBOMFormat
|
from .bom import MakeBomTemplate, ExportBom, IsValidBOMFormat
|
||||||
from order.models import PurchaseOrderLineItem
|
from order.models import PurchaseOrderLineItem
|
||||||
|
|
||||||
from .admin import PartResource
|
|
||||||
|
|
||||||
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView
|
||||||
from InvenTree.views import QRCodeView
|
from InvenTree.views import QRCodeView
|
||||||
from InvenTree.views import InvenTreeRoleMixin
|
from InvenTree.views import InvenTreeRoleMixin
|
||||||
|
|
||||||
from InvenTree.helpers import DownloadFile, str2bool
|
from InvenTree.helpers import str2bool
|
||||||
|
|
||||||
|
|
||||||
class PartIndex(InvenTreeRoleMixin, ListView):
|
class PartIndex(InvenTreeRoleMixin, ListView):
|
||||||
@@ -709,69 +707,6 @@ class BomUpload(InvenTreeRoleMixin, DetailView):
|
|||||||
template_name = 'part/upload_bom.html'
|
template_name = 'part/upload_bom.html'
|
||||||
|
|
||||||
|
|
||||||
class PartExport(AjaxView):
|
|
||||||
""" Export a CSV file containing information on multiple parts """
|
|
||||||
|
|
||||||
role_required = 'part.view'
|
|
||||||
|
|
||||||
def get_parts(self, request):
|
|
||||||
""" Extract part list from the POST parameters.
|
|
||||||
Parts can be supplied as:
|
|
||||||
|
|
||||||
- Part category
|
|
||||||
- List of part PK values
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Filter by part category
|
|
||||||
cat_id = request.GET.get('category', None)
|
|
||||||
|
|
||||||
part_list = None
|
|
||||||
|
|
||||||
if cat_id is not None:
|
|
||||||
try:
|
|
||||||
category = PartCategory.objects.get(pk=cat_id)
|
|
||||||
part_list = category.get_parts()
|
|
||||||
except (ValueError, PartCategory.DoesNotExist):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Backup - All parts
|
|
||||||
if part_list is None:
|
|
||||||
part_list = Part.objects.all()
|
|
||||||
|
|
||||||
# Also optionally filter by explicit list of part IDs
|
|
||||||
part_ids = request.GET.get('parts', '')
|
|
||||||
parts = []
|
|
||||||
|
|
||||||
for pk in part_ids.split(','):
|
|
||||||
try:
|
|
||||||
parts.append(int(pk))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if len(parts) > 0:
|
|
||||||
part_list = part_list.filter(pk__in=parts)
|
|
||||||
|
|
||||||
# Prefetch related fields to reduce DB hits
|
|
||||||
part_list = part_list.prefetch_related(
|
|
||||||
'category',
|
|
||||||
'used_in',
|
|
||||||
'builds',
|
|
||||||
'supplier_parts__purchase_order_line_items',
|
|
||||||
'stock_items__allocations',
|
|
||||||
)
|
|
||||||
|
|
||||||
return part_list
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
|
|
||||||
parts = self.get_parts(request)
|
|
||||||
|
|
||||||
dataset = PartResource().export(queryset=parts)
|
|
||||||
|
|
||||||
csv = dataset.export('csv')
|
|
||||||
return DownloadFile(csv, 'InvenTree_Parts.csv')
|
|
||||||
|
|
||||||
|
|
||||||
class BomUploadTemplate(AjaxView):
|
class BomUploadTemplate(AjaxView):
|
||||||
"""
|
"""
|
||||||
Provide a BOM upload template file for download.
|
Provide a BOM upload template file for download.
|
||||||
|
|||||||
+26
-3
@@ -30,6 +30,7 @@ from company.models import Company, SupplierPart
|
|||||||
from company.serializers import CompanySerializer, SupplierPartSerializer
|
from company.serializers import CompanySerializer, SupplierPartSerializer
|
||||||
|
|
||||||
from InvenTree.helpers import str2bool, isNull, extract_serial_numbers
|
from InvenTree.helpers import str2bool, isNull, extract_serial_numbers
|
||||||
|
from InvenTree.helpers import DownloadFile
|
||||||
from InvenTree.api import AttachmentMixin
|
from InvenTree.api import AttachmentMixin
|
||||||
from InvenTree.filters import InvenTreeOrderingFilter
|
from InvenTree.filters import InvenTreeOrderingFilter
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ from order.serializers import POSerializer
|
|||||||
from part.models import BomItem, Part, PartCategory
|
from part.models import BomItem, Part, PartCategory
|
||||||
from part.serializers import PartBriefSerializer
|
from part.serializers import PartBriefSerializer
|
||||||
|
|
||||||
|
from stock.admin import StockItemResource
|
||||||
from stock.models import StockLocation, StockItem
|
from stock.models import StockLocation, StockItem
|
||||||
from stock.models import StockItemTracking
|
from stock.models import StockItemTracking
|
||||||
from stock.models import StockItemAttachment
|
from stock.models import StockItemAttachment
|
||||||
@@ -611,6 +613,27 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
|
||||||
|
params = request.query_params
|
||||||
|
|
||||||
|
# Check if we wish to export the queried data to a file.
|
||||||
|
# If so, skip pagination!
|
||||||
|
export_format = params.get('export', None)
|
||||||
|
|
||||||
|
if export_format:
|
||||||
|
export_format = str(export_format).strip().lower()
|
||||||
|
|
||||||
|
if export_format in ['csv', 'tsv', 'xls', 'xlsx']:
|
||||||
|
dataset = StockItemResource().export(queryset=queryset)
|
||||||
|
|
||||||
|
filedata = dataset.export(export_format)
|
||||||
|
|
||||||
|
filename = 'InvenTree_Stocktake_{date}.{fmt}'.format(
|
||||||
|
date=datetime.now().strftime("%d-%b-%Y"),
|
||||||
|
fmt=export_format
|
||||||
|
)
|
||||||
|
|
||||||
|
return DownloadFile(filedata, filename)
|
||||||
|
|
||||||
page = self.paginate_queryset(queryset)
|
page = self.paginate_queryset(queryset)
|
||||||
|
|
||||||
if page is not None:
|
if page is not None:
|
||||||
@@ -641,7 +664,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
supplier_part_ids.add(sp)
|
supplier_part_ids.add(sp)
|
||||||
|
|
||||||
# Do we wish to include Part detail?
|
# Do we wish to include Part detail?
|
||||||
if str2bool(request.query_params.get('part_detail', False)):
|
if str2bool(params.get('part_detail', False)):
|
||||||
|
|
||||||
# Fetch only the required Part objects from the database
|
# Fetch only the required Part objects from the database
|
||||||
parts = Part.objects.filter(pk__in=part_ids).prefetch_related(
|
parts = Part.objects.filter(pk__in=part_ids).prefetch_related(
|
||||||
@@ -659,7 +682,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
stock_item['part_detail'] = part_map.get(part_id, None)
|
stock_item['part_detail'] = part_map.get(part_id, None)
|
||||||
|
|
||||||
# Do we wish to include SupplierPart detail?
|
# Do we wish to include SupplierPart detail?
|
||||||
if str2bool(request.query_params.get('supplier_part_detail', False)):
|
if str2bool(params.get('supplier_part_detail', False)):
|
||||||
|
|
||||||
supplier_parts = SupplierPart.objects.filter(pk__in=supplier_part_ids)
|
supplier_parts = SupplierPart.objects.filter(pk__in=supplier_part_ids)
|
||||||
|
|
||||||
@@ -673,7 +696,7 @@ class StockList(generics.ListCreateAPIView):
|
|||||||
stock_item['supplier_part_detail'] = supplier_part_map.get(part_id, None)
|
stock_item['supplier_part_detail'] = supplier_part_map.get(part_id, None)
|
||||||
|
|
||||||
# Do we wish to include StockLocation detail?
|
# Do we wish to include StockLocation detail?
|
||||||
if str2bool(request.query_params.get('location_detail', False)):
|
if str2bool(params.get('location_detail', False)):
|
||||||
|
|
||||||
# Fetch only the required StockLocation objects from the database
|
# Fetch only the required StockLocation objects from the database
|
||||||
locations = StockLocation.objects.filter(pk__in=location_ids).prefetch_related(
|
locations = StockLocation.objects.filter(pk__in=location_ids).prefetch_related(
|
||||||
|
|||||||
@@ -239,15 +239,6 @@
|
|||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
$("#stock-export").click(function() {
|
|
||||||
|
|
||||||
exportStock({
|
|
||||||
{% if location %}
|
|
||||||
location: {{ location.pk }}
|
|
||||||
{% endif %}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#location-create').click(function () {
|
$('#location-create').click(function () {
|
||||||
|
|
||||||
createStockLocation({
|
createStockLocation({
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ Unit testing for the Stock API
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
|
import tablib
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import django.http
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
@@ -261,6 +264,56 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
|
|
||||||
self.assertEqual(len(response['results']), n)
|
self.assertEqual(len(response['results']), n)
|
||||||
|
|
||||||
|
def export_data(self, filters=None):
|
||||||
|
|
||||||
|
if not filters:
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
filters['export'] = 'csv'
|
||||||
|
|
||||||
|
response = self.client.get(self.list_url, data=filters)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.assertTrue(isinstance(response, django.http.response.StreamingHttpResponse))
|
||||||
|
|
||||||
|
file_object = io.StringIO(response.getvalue().decode('utf-8'))
|
||||||
|
|
||||||
|
dataset = tablib.Dataset().load(file_object, 'csv', headers=True)
|
||||||
|
|
||||||
|
return dataset
|
||||||
|
|
||||||
|
def test_export(self):
|
||||||
|
"""
|
||||||
|
Test exporting of Stock data via the API
|
||||||
|
"""
|
||||||
|
|
||||||
|
dataset = self.export_data({})
|
||||||
|
|
||||||
|
self.assertEqual(len(dataset), 20)
|
||||||
|
|
||||||
|
# Expected headers
|
||||||
|
headers = [
|
||||||
|
'part',
|
||||||
|
'customer',
|
||||||
|
'location',
|
||||||
|
'parent',
|
||||||
|
'quantity',
|
||||||
|
'status',
|
||||||
|
]
|
||||||
|
|
||||||
|
for h in headers:
|
||||||
|
self.assertIn(h, dataset.headers)
|
||||||
|
|
||||||
|
# Now, add a filter to the results
|
||||||
|
dataset = self.export_data({'location': 1})
|
||||||
|
|
||||||
|
self.assertEqual(len(dataset), 2)
|
||||||
|
|
||||||
|
dataset = self.export_data({'part': 25})
|
||||||
|
|
||||||
|
self.assertEqual(len(dataset), 8)
|
||||||
|
|
||||||
|
|
||||||
class StockItemTest(StockAPITestCase):
|
class StockItemTest(StockAPITestCase):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ stock_urls = [
|
|||||||
|
|
||||||
url(r'^track/', include(stock_tracking_urls)),
|
url(r'^track/', include(stock_tracking_urls)),
|
||||||
|
|
||||||
url(r'^export/?', views.StockExport.as_view(), name='stock-export'),
|
|
||||||
|
|
||||||
# Individual stock items
|
# Individual stock items
|
||||||
url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)),
|
url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)),
|
||||||
|
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ from InvenTree.views import QRCodeView
|
|||||||
from InvenTree.views import InvenTreeRoleMixin
|
from InvenTree.views import InvenTreeRoleMixin
|
||||||
from InvenTree.forms import ConfirmForm
|
from InvenTree.forms import ConfirmForm
|
||||||
|
|
||||||
from InvenTree.helpers import str2bool, DownloadFile, GetExportFormats
|
from InvenTree.helpers import str2bool
|
||||||
from InvenTree.helpers import extract_serial_numbers
|
from InvenTree.helpers import extract_serial_numbers
|
||||||
|
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from company.models import Company, SupplierPart
|
from company.models import SupplierPart
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
from .models import StockItem, StockLocation, StockItemTracking
|
from .models import StockItem, StockLocation, StockItemTracking
|
||||||
|
|
||||||
@@ -39,8 +39,6 @@ import common.settings
|
|||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from users.models import Owner
|
from users.models import Owner
|
||||||
|
|
||||||
from .admin import StockItemResource
|
|
||||||
|
|
||||||
from . import forms as StockForms
|
from . import forms as StockForms
|
||||||
|
|
||||||
|
|
||||||
@@ -380,95 +378,6 @@ class StockItemDeleteTestData(AjaxUpdateView):
|
|||||||
return self.renderJsonResponse(request, form, data)
|
return self.renderJsonResponse(request, form, data)
|
||||||
|
|
||||||
|
|
||||||
class StockExport(AjaxView):
|
|
||||||
""" Export stock data from a particular location.
|
|
||||||
Returns a file containing stock information for that location.
|
|
||||||
"""
|
|
||||||
|
|
||||||
model = StockItem
|
|
||||||
role_required = 'stock.view'
|
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
|
|
||||||
export_format = request.GET.get('format', 'csv').lower()
|
|
||||||
|
|
||||||
# Check if a particular location was specified
|
|
||||||
loc_id = request.GET.get('location', None)
|
|
||||||
location = None
|
|
||||||
|
|
||||||
if loc_id:
|
|
||||||
try:
|
|
||||||
location = StockLocation.objects.get(pk=loc_id)
|
|
||||||
except (ValueError, StockLocation.DoesNotExist):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Check if a particular supplier was specified
|
|
||||||
sup_id = request.GET.get('supplier', None)
|
|
||||||
supplier = None
|
|
||||||
|
|
||||||
if sup_id:
|
|
||||||
try:
|
|
||||||
supplier = Company.objects.get(pk=sup_id)
|
|
||||||
except (ValueError, Company.DoesNotExist):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Check if a particular supplier_part was specified
|
|
||||||
sup_part_id = request.GET.get('supplier_part', None)
|
|
||||||
supplier_part = None
|
|
||||||
|
|
||||||
if sup_part_id:
|
|
||||||
try:
|
|
||||||
supplier_part = SupplierPart.objects.get(pk=sup_part_id)
|
|
||||||
except (ValueError, SupplierPart.DoesNotExist):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Check if a particular part was specified
|
|
||||||
part_id = request.GET.get('part', None)
|
|
||||||
part = None
|
|
||||||
|
|
||||||
if part_id:
|
|
||||||
try:
|
|
||||||
part = Part.objects.get(pk=part_id)
|
|
||||||
except (ValueError, Part.DoesNotExist):
|
|
||||||
pass
|
|
||||||
|
|
||||||
if export_format not in GetExportFormats():
|
|
||||||
export_format = 'csv'
|
|
||||||
|
|
||||||
filename = 'InvenTree_Stocktake_{date}.{fmt}'.format(
|
|
||||||
date=datetime.now().strftime("%d-%b-%Y"),
|
|
||||||
fmt=export_format
|
|
||||||
)
|
|
||||||
|
|
||||||
if location:
|
|
||||||
# Check if locations should be cascading
|
|
||||||
cascade = str2bool(request.GET.get('cascade', True))
|
|
||||||
stock_items = location.get_stock_items(cascade)
|
|
||||||
else:
|
|
||||||
stock_items = StockItem.objects.all()
|
|
||||||
|
|
||||||
if part:
|
|
||||||
stock_items = stock_items.filter(part=part)
|
|
||||||
|
|
||||||
if supplier:
|
|
||||||
stock_items = stock_items.filter(supplier_part__supplier=supplier)
|
|
||||||
|
|
||||||
if supplier_part:
|
|
||||||
stock_items = stock_items.filter(supplier_part=supplier_part)
|
|
||||||
|
|
||||||
# Filter out stock items that are not 'in stock'
|
|
||||||
stock_items = stock_items.filter(StockItem.IN_STOCK_FILTER)
|
|
||||||
|
|
||||||
# Pre-fetch related fields to reduce DB queries
|
|
||||||
stock_items = stock_items.prefetch_related('part', 'supplier_part__supplier', 'location', 'purchase_order', 'build')
|
|
||||||
|
|
||||||
dataset = StockItemResource().export(queryset=stock_items)
|
|
||||||
|
|
||||||
filedata = dataset.export(export_format)
|
|
||||||
|
|
||||||
return DownloadFile(filedata, filename)
|
|
||||||
|
|
||||||
|
|
||||||
class StockItemQRCode(QRCodeView):
|
class StockItemQRCode(QRCodeView):
|
||||||
""" View for displaying a QR code for a StockItem object """
|
""" View for displaying a QR code for a StockItem object """
|
||||||
|
|
||||||
|
|||||||
@@ -671,9 +671,7 @@ function loadBomTable(table, options={}) {
|
|||||||
// Do we show part pricing in the BOM table?
|
// Do we show part pricing in the BOM table?
|
||||||
var show_pricing = global_settings.PART_SHOW_PRICE_IN_BOM;
|
var show_pricing = global_settings.PART_SHOW_PRICE_IN_BOM;
|
||||||
|
|
||||||
if (!show_pricing) {
|
params.include_pricing = show_pricing == true;
|
||||||
params.include_pricing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.part_detail) {
|
if (options.part_detail) {
|
||||||
params.part_detail = true;
|
params.part_detail = true;
|
||||||
@@ -989,32 +987,40 @@ function loadBomTable(table, options={}) {
|
|||||||
|
|
||||||
// Function to request BOM data for sub-items
|
// Function to request BOM data for sub-items
|
||||||
// This function may be called recursively for multi-level BOMs
|
// This function may be called recursively for multi-level BOMs
|
||||||
function requestSubItems(bom_pk, part_pk) {
|
function requestSubItems(bom_pk, part_pk, depth=0) {
|
||||||
|
|
||||||
// TODO: 2022-02-03 Currently, multi-level BOMs are not actually displayed.
|
// Prevent multi-level recursion
|
||||||
|
const MAX_BOM_DEPTH = 25;
|
||||||
|
|
||||||
// Re-enable this function once multi-level display has been re-deployed
|
if (depth >= MAX_BOM_DEPTH) {
|
||||||
return;
|
console.log(`Maximum BOM depth (${MAX_BOM_DEPTH}) reached!`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
inventreeGet(
|
inventreeGet(
|
||||||
options.bom_url,
|
options.bom_url,
|
||||||
{
|
{
|
||||||
part: part_pk,
|
part: part_pk,
|
||||||
sub_part_detail: true,
|
sub_part_detail: true,
|
||||||
|
include_pricing: show_pricing == true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
|
|
||||||
|
// Add the returned sub-items to the table
|
||||||
for (var idx = 0; idx < response.length; idx++) {
|
for (var idx = 0; idx < response.length; idx++) {
|
||||||
|
|
||||||
response[idx].parentId = bom_pk;
|
response[idx].parentId = bom_pk;
|
||||||
|
|
||||||
if (response[idx].sub_part_detail.assembly) {
|
|
||||||
requestSubItems(response[idx].pk, response[idx].sub_part);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table.bootstrapTable('append', response);
|
table.bootstrapTable('append', response);
|
||||||
|
|
||||||
|
// Next, re-iterate and check if the new items also have sub items
|
||||||
|
response.forEach(function(bom_item) {
|
||||||
|
if (bom_item.sub_part_detail.assembly) {
|
||||||
|
requestSubItems(bom_item.pk, bom_item.sub_part, depth + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
table.treegrid('collapseAll');
|
table.treegrid('collapseAll');
|
||||||
},
|
},
|
||||||
error: function(xhr) {
|
error: function(xhr) {
|
||||||
@@ -1026,7 +1032,7 @@ function loadBomTable(table, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.inventreeTable({
|
table.inventreeTable({
|
||||||
treeEnable: !options.editable,
|
treeEnable: true,
|
||||||
rootParentId: parent_id,
|
rootParentId: parent_id,
|
||||||
idField: 'pk',
|
idField: 'pk',
|
||||||
uniqueId: 'pk',
|
uniqueId: 'pk',
|
||||||
@@ -1066,38 +1072,37 @@ function loadBomTable(table, options={}) {
|
|||||||
url: options.bom_url,
|
url: options.bom_url,
|
||||||
onPostBody: function() {
|
onPostBody: function() {
|
||||||
|
|
||||||
if (!options.editable) {
|
table.treegrid({
|
||||||
table.treegrid({
|
treeColumn: 1,
|
||||||
treeColumn: 0,
|
onExpand: function() {
|
||||||
onExpand: function() {
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
table.treegrid('collapseAll');
|
||||||
},
|
},
|
||||||
onLoadSuccess: function() {
|
onLoadSuccess: function() {
|
||||||
|
|
||||||
if (options.editable) {
|
if (options.editable) {
|
||||||
table.bootstrapTable('uncheckAll');
|
table.bootstrapTable('uncheckAll');
|
||||||
} else {
|
}
|
||||||
|
|
||||||
var data = table.bootstrapTable('getData');
|
var data = table.bootstrapTable('getData');
|
||||||
|
|
||||||
for (var idx = 0; idx < data.length; idx++) {
|
for (var idx = 0; idx < data.length; idx++) {
|
||||||
var row = data[idx];
|
var row = data[idx];
|
||||||
|
|
||||||
// If a row already has a parent ID set, it's already been updated!
|
// If a row already has a parent ID set, it's already been updated!
|
||||||
if (row.parentId) {
|
if (row.parentId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the parent ID of the top-level rows
|
// Set the parent ID of the top-level rows
|
||||||
row.parentId = parent_id;
|
row.parentId = parent_id;
|
||||||
|
|
||||||
table.bootstrapTable('updateRow', idx, row, true);
|
table.bootstrapTable('updateRow', idx, row, true);
|
||||||
|
|
||||||
if (row.sub_part_detail.assembly) {
|
if (row.sub_part_detail.assembly) {
|
||||||
requestSubItems(row.pk, row.sub_part);
|
requestSubItems(row.pk, row.sub_part);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ function generateFilterInput(tableKey, filterKey) {
|
|||||||
* @param {*} table - bootstrapTable element to update
|
* @param {*} table - bootstrapTable element to update
|
||||||
* @param {*} target - name of target element on page
|
* @param {*} target - name of target element on page
|
||||||
*/
|
*/
|
||||||
function setupFilterList(tableKey, table, target) {
|
function setupFilterList(tableKey, table, target, options={}) {
|
||||||
|
|
||||||
var addClicked = false;
|
var addClicked = false;
|
||||||
|
|
||||||
@@ -283,6 +283,11 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
|
|
||||||
var buttons = '';
|
var buttons = '';
|
||||||
|
|
||||||
|
// Add download button
|
||||||
|
if (options.download) {
|
||||||
|
buttons += `<button id='download-${tableKey}' title='{% trans "Download data" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-download'></span></button>`;
|
||||||
|
}
|
||||||
|
|
||||||
buttons += `<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-redo-alt'></span></button>`;
|
buttons += `<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-redo-alt'></span></button>`;
|
||||||
|
|
||||||
// If there are filters defined for this table, add more buttons
|
// If there are filters defined for this table, add more buttons
|
||||||
@@ -295,7 +300,7 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
element.html(`
|
element.html(`
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group filter-group' role='group'>
|
||||||
${buttons}
|
${buttons}
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
@@ -322,6 +327,13 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
$(table).bootstrapTable('refresh');
|
$(table).bootstrapTable('refresh');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add a callback for downloading table data
|
||||||
|
if (options.download) {
|
||||||
|
element.find(`#download-${tableKey}`).click(function() {
|
||||||
|
downloadTableData($(table));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Add a callback for adding a new filter
|
// Add a callback for adding a new filter
|
||||||
element.find(`#${add}`).click(function clicked() {
|
element.find(`#${add}`).click(function clicked() {
|
||||||
|
|
||||||
@@ -358,14 +370,14 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
reloadTableFilters(table, filters);
|
reloadTableFilters(table, filters);
|
||||||
|
|
||||||
// Run this function again
|
// Run this function again
|
||||||
setupFilterList(tableKey, table, target);
|
setupFilterList(tableKey, table, target, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
addClicked = false;
|
addClicked = false;
|
||||||
|
|
||||||
setupFilterList(tableKey, table, target);
|
setupFilterList(tableKey, table, target, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -376,7 +388,7 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
|
|
||||||
reloadTableFilters(table, filters);
|
reloadTableFilters(table, filters);
|
||||||
|
|
||||||
setupFilterList(tableKey, table, target);
|
setupFilterList(tableKey, table, target, options);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add callback for deleting each filter
|
// Add callback for deleting each filter
|
||||||
@@ -390,7 +402,7 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
reloadTableFilters(table, filters);
|
reloadTableFilters(table, filters);
|
||||||
|
|
||||||
// Run this function again!
|
// Run this function again!
|
||||||
setupFilterList(tableKey, table, target);
|
setupFilterList(tableKey, table, target, options);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -281,6 +281,65 @@ function createPurchaseOrder(options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Construct a set of fields for the SalesOrderLineItem form */
|
||||||
|
function soLineItemFields(options={}) {
|
||||||
|
|
||||||
|
var fields = {
|
||||||
|
order: {
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
part: {},
|
||||||
|
quantity: {},
|
||||||
|
reference: {},
|
||||||
|
sale_price: {},
|
||||||
|
sale_price_currency: {},
|
||||||
|
target_date: {},
|
||||||
|
notes: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.order) {
|
||||||
|
fields.order.value = options.order;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Construct a set of fields for the PurchaseOrderLineItem form */
|
||||||
|
function poLineItemFields(options={}) {
|
||||||
|
|
||||||
|
var fields = {
|
||||||
|
order: {
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
part: {
|
||||||
|
filters: {
|
||||||
|
part_detail: true,
|
||||||
|
supplier_detail: true,
|
||||||
|
supplier: options.supplier,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
quantity: {},
|
||||||
|
reference: {},
|
||||||
|
purchase_price: {},
|
||||||
|
purchase_price_currency: {},
|
||||||
|
target_date: {},
|
||||||
|
destination: {},
|
||||||
|
notes: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.order) {
|
||||||
|
fields.order.value = options.order;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.currency) {
|
||||||
|
fields.purchase_price_currency.value = options.currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function removeOrderRowFromOrderWizard(e) {
|
function removeOrderRowFromOrderWizard(e) {
|
||||||
/* Remove a part selection from an order form. */
|
/* Remove a part selection from an order form. */
|
||||||
|
|
||||||
@@ -293,6 +352,7 @@ function removeOrderRowFromOrderWizard(e) {
|
|||||||
$('#' + row).remove();
|
$('#' + row).remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function newSupplierPartFromOrderWizard(e) {
|
function newSupplierPartFromOrderWizard(e) {
|
||||||
/* Create a new supplier part directly from an order form.
|
/* Create a new supplier part directly from an order form.
|
||||||
* Launches a secondary modal and (if successful),
|
* Launches a secondary modal and (if successful),
|
||||||
@@ -991,10 +1051,36 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
|||||||
|
|
||||||
var target = options.filter_target || '#filter-list-purchase-order-lines';
|
var target = options.filter_target || '#filter-list-purchase-order-lines';
|
||||||
|
|
||||||
setupFilterList('purchaseorderlineitem', $(table), target);
|
setupFilterList('purchaseorderlineitem', $(table), target, {download: true});
|
||||||
|
|
||||||
function setupCallbacks() {
|
function setupCallbacks() {
|
||||||
if (options.allow_edit) {
|
if (options.allow_edit) {
|
||||||
|
|
||||||
|
// Callback for "duplicate" button
|
||||||
|
$(table).find('.button-line-duplicate').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
inventreeGet(`/api/order/po-line/${pk}/`, {}, {
|
||||||
|
success: function(data) {
|
||||||
|
|
||||||
|
var fields = poLineItemFields({
|
||||||
|
supplier: options.supplier,
|
||||||
|
});
|
||||||
|
|
||||||
|
constructForm('{% url "api-po-line-list" %}', {
|
||||||
|
method: 'POST',
|
||||||
|
fields: fields,
|
||||||
|
data: data,
|
||||||
|
title: '{% trans "Duplicate Line Item" %}',
|
||||||
|
onSuccess: function(response) {
|
||||||
|
$(table).bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Callback for "edit" button
|
||||||
$(table).find('.button-line-edit').click(function() {
|
$(table).find('.button-line-edit').click(function() {
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
@@ -1022,6 +1108,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Callback for "delete" button
|
||||||
$(table).find('.button-line-delete').click(function() {
|
$(table).find('.button-line-delete').click(function() {
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
@@ -1270,6 +1357,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.allow_edit) {
|
if (options.allow_edit) {
|
||||||
|
html += makeIconButton('fa-clone', 'button-line-duplicate', pk, '{% trans "Duplicate line item" %}');
|
||||||
html += makeIconButton('fa-edit icon-blue', 'button-line-edit', pk, '{% trans "Edit line item" %}');
|
html += makeIconButton('fa-edit icon-blue', 'button-line-edit', pk, '{% trans "Edit line item" %}');
|
||||||
html += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete line item" %}');
|
html += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete line item" %}');
|
||||||
}
|
}
|
||||||
@@ -2449,6 +2537,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
|
|||||||
html += makeIconButton('fa-dollar-sign icon-green', 'button-price', pk, '{% trans "Calculate price" %}');
|
html += makeIconButton('fa-dollar-sign icon-green', 'button-price', pk, '{% trans "Calculate price" %}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html += makeIconButton('fa-clone', 'button-duplicate', pk, '{% trans "Duplicate line item" %}');
|
||||||
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}');
|
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}');
|
||||||
|
|
||||||
var delete_disabled = false;
|
var delete_disabled = false;
|
||||||
@@ -2480,6 +2569,28 @@ function loadSalesOrderLineItemTable(table, options={}) {
|
|||||||
// Configure callback functions once the table is loaded
|
// Configure callback functions once the table is loaded
|
||||||
function setupCallbacks() {
|
function setupCallbacks() {
|
||||||
|
|
||||||
|
// Callback for duplicating line items
|
||||||
|
$(table).find('.button-duplicate').click(function() {
|
||||||
|
var pk = $(this).attr('pk');
|
||||||
|
|
||||||
|
inventreeGet(`/api/order/so-line/${pk}/`, {}, {
|
||||||
|
success: function(data) {
|
||||||
|
|
||||||
|
var fields = soLineItemFields();
|
||||||
|
|
||||||
|
constructForm('{% url "api-so-line-list" %}', {
|
||||||
|
method: 'POST',
|
||||||
|
fields: fields,
|
||||||
|
data: data,
|
||||||
|
title: '{% trans "Duplicate Line Item" %}',
|
||||||
|
onSuccess: function(response) {
|
||||||
|
$(table).bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Callback for editing line items
|
// Callback for editing line items
|
||||||
$(table).find('.button-edit').click(function() {
|
$(table).find('.button-edit').click(function() {
|
||||||
var pk = $(this).attr('pk');
|
var pk = $(this).attr('pk');
|
||||||
|
|||||||
@@ -474,6 +474,7 @@ function duplicateBom(part_id, options={}) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
include_inherited: {},
|
include_inherited: {},
|
||||||
|
copy_substitutes: {},
|
||||||
remove_existing: {},
|
remove_existing: {},
|
||||||
skip_invalid: {},
|
skip_invalid: {},
|
||||||
},
|
},
|
||||||
@@ -1217,7 +1218,7 @@ function loadPartTable(table, url, options={}) {
|
|||||||
filters[key] = params[key];
|
filters[key] = params[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
setupFilterList('parts', $(table), options.filterTarget || null);
|
setupFilterList('parts', $(table), options.filterTarget, {download: true});
|
||||||
|
|
||||||
var columns = [
|
var columns = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,7 +43,6 @@
|
|||||||
duplicateStockItem,
|
duplicateStockItem,
|
||||||
editStockItem,
|
editStockItem,
|
||||||
editStockLocation,
|
editStockLocation,
|
||||||
exportStock,
|
|
||||||
findStockItemBySerialNumber,
|
findStockItemBySerialNumber,
|
||||||
installStockItem,
|
installStockItem,
|
||||||
loadInstalledInTable,
|
loadInstalledInTable,
|
||||||
@@ -506,49 +505,6 @@ function stockStatusCodes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Export stock table
|
|
||||||
*/
|
|
||||||
function exportStock(params={}) {
|
|
||||||
|
|
||||||
constructFormBody({}, {
|
|
||||||
title: '{% trans "Export Stock" %}',
|
|
||||||
fields: {
|
|
||||||
format: {
|
|
||||||
label: '{% trans "Format" %}',
|
|
||||||
help_text: '{% trans "Select file format" %}',
|
|
||||||
required: true,
|
|
||||||
type: 'choice',
|
|
||||||
value: 'csv',
|
|
||||||
choices: exportFormatOptions(),
|
|
||||||
},
|
|
||||||
sublocations: {
|
|
||||||
label: '{% trans "Include Sublocations" %}',
|
|
||||||
help_text: '{% trans "Include stock items in sublocations" %}',
|
|
||||||
type: 'boolean',
|
|
||||||
value: 'true',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSubmit: function(fields, form_options) {
|
|
||||||
|
|
||||||
var format = getFormFieldValue('format', fields['format'], form_options);
|
|
||||||
var cascade = getFormFieldValue('sublocations', fields['sublocations'], form_options);
|
|
||||||
|
|
||||||
// Hide the modal
|
|
||||||
$(form_options.modal).modal('hide');
|
|
||||||
|
|
||||||
var url = `{% url "stock-export" %}?format=${format}&cascade=${cascade}`;
|
|
||||||
|
|
||||||
for (var key in params) {
|
|
||||||
url += `&${key}=${params[key]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
location.href = url;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assign multiple stock items to a customer
|
* Assign multiple stock items to a customer
|
||||||
*/
|
*/
|
||||||
@@ -1615,7 +1571,7 @@ function loadStockTable(table, options) {
|
|||||||
original[k] = params[k];
|
original[k] = params[k];
|
||||||
}
|
}
|
||||||
|
|
||||||
setupFilterList(filterKey, table, filterTarget);
|
setupFilterList(filterKey, table, filterTarget, {download: true});
|
||||||
|
|
||||||
// Override the default values, or add new ones
|
// Override the default values, or add new ones
|
||||||
for (var key in params) {
|
for (var key in params) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
/* exported
|
/* exported
|
||||||
customGroupSorter,
|
customGroupSorter,
|
||||||
|
downloadTableData,
|
||||||
reloadtable,
|
reloadtable,
|
||||||
renderLink,
|
renderLink,
|
||||||
reloadTableFilters,
|
reloadTableFilters,
|
||||||
@@ -21,6 +22,62 @@ function reloadtable(table) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download data from a table, via the API.
|
||||||
|
* This requires a number of conditions to be met:
|
||||||
|
*
|
||||||
|
* - The API endpoint supports data download (on the server side)
|
||||||
|
* - The table is "flat" (does not support multi-level loading, etc)
|
||||||
|
* - The table has been loaded using the inventreeTable() function, not bootstrapTable()
|
||||||
|
* (Refer to the "reloadTableFilters" function to see why!)
|
||||||
|
*/
|
||||||
|
function downloadTableData(table, opts={}) {
|
||||||
|
|
||||||
|
// Extract table configuration options
|
||||||
|
var table_options = table.bootstrapTable('getOptions');
|
||||||
|
|
||||||
|
var url = table_options.url;
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
console.log('Error: downloadTableData could not find "url" parameter.');
|
||||||
|
}
|
||||||
|
|
||||||
|
var query_params = table_options.query_params || {};
|
||||||
|
|
||||||
|
url += '?';
|
||||||
|
|
||||||
|
constructFormBody({}, {
|
||||||
|
title: opts.title || '{% trans "Export Table Data" %}',
|
||||||
|
fields: {
|
||||||
|
format: {
|
||||||
|
label: '{% trans "Format" %}',
|
||||||
|
help_text: '{% trans "Select File Format" %}',
|
||||||
|
required: true,
|
||||||
|
type: 'choice',
|
||||||
|
value: 'csv',
|
||||||
|
choices: exportFormatOptions(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSubmit: function(fields, form_options) {
|
||||||
|
var format = getFormFieldValue('format', fields['format'], form_options);
|
||||||
|
|
||||||
|
// Hide the modal
|
||||||
|
$(form_options.modal).modal('hide');
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(query_params)) {
|
||||||
|
url += `${key}=${value}&`;
|
||||||
|
}
|
||||||
|
|
||||||
|
url += `export=${format}`;
|
||||||
|
|
||||||
|
location.href = url;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a URL for display
|
* Render a URL for display
|
||||||
* @param {String} text
|
* @param {String} text
|
||||||
@@ -114,6 +171,10 @@ function reloadTableFilters(table, filters) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the total set of query params
|
||||||
|
// This is necessary for the "downloadTableData" function to work
|
||||||
|
options.query_params = params;
|
||||||
|
|
||||||
options.queryParams = function(tableParams) {
|
options.queryParams = function(tableParams) {
|
||||||
return convertQueryParameters(tableParams, params);
|
return convertQueryParameters(tableParams, params);
|
||||||
};
|
};
|
||||||
@@ -221,7 +282,11 @@ $.fn.inventreeTable = function(options) {
|
|||||||
// Extract query params
|
// Extract query params
|
||||||
var filters = options.queryParams || options.filters || {};
|
var filters = options.queryParams || options.filters || {};
|
||||||
|
|
||||||
|
// Store the total set of query params
|
||||||
|
options.query_params = filters;
|
||||||
|
|
||||||
options.queryParams = function(params) {
|
options.queryParams = function(params) {
|
||||||
|
// Update the query parameters callback with the *new* filters
|
||||||
return convertQueryParameters(params, filters);
|
return convertQueryParameters(params, filters);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,6 @@
|
|||||||
<div id='{{ prefix }}button-toolbar'>
|
<div id='{{ prefix }}button-toolbar'>
|
||||||
<div class='button-toolbar container-fluid' style='float: right;'>
|
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
<button class='btn btn-outline-secondary' id='stock-export' title='{% trans "Export Stock Information" %}'>
|
|
||||||
<span class='fas fa-download'></span>
|
|
||||||
</button>
|
|
||||||
{% if barcodes %}
|
{% if barcodes %}
|
||||||
<!-- Barcode actions menu -->
|
<!-- Barcode actions menu -->
|
||||||
<div class='btn-group' role='group'>
|
<div class='btn-group' role='group'>
|
||||||
|
|||||||
Reference in new issue
Block a user