mirror of
https://github.com/inventree/InvenTree.git
synced 2026-06-14 20:40:45 +00:00
Merged master
This commit is contained in:
@@ -13,6 +13,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
- name: Check Release tag
|
||||||
|
run: |
|
||||||
|
python3 ci/check_version_number.py ${{ github.event.release.tag_name }}
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
|
|||||||
@@ -344,13 +344,15 @@ def GetExportFormats():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def DownloadFile(data, filename, content_type='application/text'):
|
def DownloadFile(data, filename, content_type='application/text', inline=False):
|
||||||
""" Create a dynamic file for the user to download.
|
"""
|
||||||
|
Create a dynamic file for the user to download.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: Raw file data (string or bytes)
|
data: Raw file data (string or bytes)
|
||||||
filename: Filename for the file download
|
filename: Filename for the file download
|
||||||
content_type: Content type for the download
|
content_type: Content type for the download
|
||||||
|
inline: Download "inline" or as attachment? (Default = attachment)
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
A StreamingHttpResponse object wrapping the supplied data
|
A StreamingHttpResponse object wrapping the supplied data
|
||||||
@@ -365,7 +367,10 @@ def DownloadFile(data, filename, content_type='application/text'):
|
|||||||
|
|
||||||
response = StreamingHttpResponse(wrapper, content_type=content_type)
|
response = StreamingHttpResponse(wrapper, content_type=content_type)
|
||||||
response['Content-Length'] = len(data)
|
response['Content-Length'] = len(data)
|
||||||
response['Content-Disposition'] = 'attachment; filename={f}'.format(f=filename)
|
|
||||||
|
disposition = "inline" if inline else "attachment"
|
||||||
|
|
||||||
|
response['Content-Disposition'] = f'{disposition}; filename={filename}'
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -21,28 +21,15 @@ class AuthRequiredMiddleware(object):
|
|||||||
|
|
||||||
assert hasattr(request, 'user')
|
assert hasattr(request, 'user')
|
||||||
|
|
||||||
response = self.get_response(request)
|
# API requests are handled by the DRF library
|
||||||
|
if request.path_info.startswith('/api/'):
|
||||||
|
return self.get_response(request)
|
||||||
|
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
"""
|
"""
|
||||||
Normally, a web-based session would use csrftoken based authentication.
|
Normally, a web-based session would use csrftoken based authentication.
|
||||||
However when running an external application (e.g. the InvenTree app),
|
However when running an external application (e.g. the InvenTree app or Python library),
|
||||||
we wish to use token-based auth to grab media files.
|
we must validate the user token manually.
|
||||||
|
|
||||||
So, we will allow token-based authentication but ONLY for the /media/ directory.
|
|
||||||
|
|
||||||
What problem is this solving?
|
|
||||||
- The InvenTree mobile app does not use csrf token auth
|
|
||||||
- Token auth is used by the Django REST framework, but that is under the /api/ endpoint
|
|
||||||
- Media files (e.g. Part images) are required to be served to the app
|
|
||||||
- We do not want to make /media/ files accessible without login!
|
|
||||||
|
|
||||||
There is PROBABLY a better way of going about this?
|
|
||||||
a) Allow token-based authentication against a user?
|
|
||||||
b) Serve /media/ files in a duplicate location e.g. /api/media/ ?
|
|
||||||
c) Is there a "standard" way of solving this problem?
|
|
||||||
|
|
||||||
My [google|stackoverflow]-fu has failed me. So this hack has been created.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
authorized = False
|
authorized = False
|
||||||
@@ -56,21 +43,24 @@ class AuthRequiredMiddleware(object):
|
|||||||
elif request.path_info.startswith('/accounts/'):
|
elif request.path_info.startswith('/accounts/'):
|
||||||
authorized = True
|
authorized = True
|
||||||
|
|
||||||
elif 'Authorization' in request.headers.keys():
|
elif 'Authorization' in request.headers.keys() or 'authorization' in request.headers.keys():
|
||||||
auth = request.headers['Authorization'].strip()
|
auth = request.headers.get('Authorization', request.headers.get('authorization')).strip()
|
||||||
|
|
||||||
if auth.startswith('Token') and len(auth.split()) == 2:
|
if auth.lower().startswith('token') and len(auth.split()) == 2:
|
||||||
token = auth.split()[1]
|
token_key = auth.split()[1]
|
||||||
|
|
||||||
# Does the provided token match a valid user?
|
# Does the provided token match a valid user?
|
||||||
if Token.objects.filter(key=token).exists():
|
try:
|
||||||
|
token = Token.objects.get(key=token_key)
|
||||||
|
|
||||||
allowed = ['/api/', '/media/']
|
# Provide the user information to the request
|
||||||
|
request.user = token.user
|
||||||
# Only allow token-auth for /media/ or /static/ dirs!
|
|
||||||
if any([request.path_info.startswith(a) for a in allowed]):
|
|
||||||
authorized = True
|
authorized = True
|
||||||
|
|
||||||
|
except Token.DoesNotExist:
|
||||||
|
logger.warning(f"Access denied for unknown token {token_key}")
|
||||||
|
pass
|
||||||
|
|
||||||
# No authorization was found for the request
|
# No authorization was found for the request
|
||||||
if not authorized:
|
if not authorized:
|
||||||
# A logout request will redirect the user to the login screen
|
# A logout request will redirect the user to the login screen
|
||||||
@@ -92,8 +82,7 @@ class AuthRequiredMiddleware(object):
|
|||||||
|
|
||||||
return redirect('%s?next=%s' % (reverse_lazy('login'), request.path))
|
return redirect('%s?next=%s' % (reverse_lazy('login'), request.path))
|
||||||
|
|
||||||
# Code to be executed for each request/response after
|
response = self.get_response(request)
|
||||||
# the view is called.
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import json
|
|||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from django.core.exceptions import AppRegistryNotReady
|
from django.core.exceptions import AppRegistryNotReady
|
||||||
from django.db.utils import OperationalError, ProgrammingError
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
@@ -51,11 +52,14 @@ def schedule_task(taskname, **kwargs):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def offload_task(taskname, *args, **kwargs):
|
def offload_task(taskname, force_sync=False, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Create an AsyncTask.
|
Create an AsyncTask if workers are running.
|
||||||
This is different to a 'scheduled' task,
|
This is different to a 'scheduled' task,
|
||||||
in that it only runs once!
|
in that it only runs once!
|
||||||
|
|
||||||
|
If workers are not running or force_sync flag
|
||||||
|
is set then the task is ran synchronously.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -63,10 +67,48 @@ def offload_task(taskname, *args, **kwargs):
|
|||||||
except (AppRegistryNotReady):
|
except (AppRegistryNotReady):
|
||||||
logger.warning("Could not offload task - app registry not ready")
|
logger.warning("Could not offload task - app registry not ready")
|
||||||
return
|
return
|
||||||
|
import importlib
|
||||||
|
from InvenTree.status import is_worker_running
|
||||||
|
|
||||||
|
if is_worker_running() and not force_sync:
|
||||||
|
# Running as asynchronous task
|
||||||
|
try:
|
||||||
task = AsyncTask(taskname, *args, **kwargs)
|
task = AsyncTask(taskname, *args, **kwargs)
|
||||||
|
|
||||||
task.run()
|
task.run()
|
||||||
|
except ImportError:
|
||||||
|
logger.warning(f"WARNING: '{taskname}' not started - Function not found")
|
||||||
|
else:
|
||||||
|
# Split path
|
||||||
|
try:
|
||||||
|
app, mod, func = taskname.split('.')
|
||||||
|
app_mod = app + '.' + mod
|
||||||
|
except ValueError:
|
||||||
|
logger.warning(f"WARNING: '{taskname}' not started - Malformed function path")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Import module from app
|
||||||
|
try:
|
||||||
|
_mod = importlib.import_module(app_mod)
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
logger.warning(f"WARNING: '{taskname}' not started - No module named '{app_mod}'")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Retrieve function
|
||||||
|
try:
|
||||||
|
_func = getattr(_mod, func)
|
||||||
|
except AttributeError:
|
||||||
|
# getattr does not work for local import
|
||||||
|
_func = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not _func:
|
||||||
|
_func = eval(func)
|
||||||
|
except NameError:
|
||||||
|
logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Workers are not running: run it as synchronous task
|
||||||
|
_func()
|
||||||
|
|
||||||
|
|
||||||
def heartbeat():
|
def heartbeat():
|
||||||
@@ -84,7 +126,7 @@ def heartbeat():
|
|||||||
except AppRegistryNotReady:
|
except AppRegistryNotReady:
|
||||||
return
|
return
|
||||||
|
|
||||||
threshold = datetime.now() - timedelta(minutes=30)
|
threshold = timezone.now() - timedelta(minutes=30)
|
||||||
|
|
||||||
# Delete heartbeat results more than half an hour old,
|
# Delete heartbeat results more than half an hour old,
|
||||||
# otherwise they just create extra noise
|
# otherwise they just create extra noise
|
||||||
@@ -108,7 +150,7 @@ def delete_successful_tasks():
|
|||||||
logger.info("Could not perform 'delete_successful_tasks' - App registry not ready")
|
logger.info("Could not perform 'delete_successful_tasks' - App registry not ready")
|
||||||
return
|
return
|
||||||
|
|
||||||
threshold = datetime.now() - timedelta(days=30)
|
threshold = timezone.now() - timedelta(days=30)
|
||||||
|
|
||||||
results = Success.objects.filter(
|
results = Success.objects.filter(
|
||||||
started__lte=threshold
|
started__lte=threshold
|
||||||
|
|||||||
@@ -10,11 +10,14 @@ import common.models
|
|||||||
|
|
||||||
INVENTREE_SW_VERSION = "0.5.0 pre"
|
INVENTREE_SW_VERSION = "0.5.0 pre"
|
||||||
|
|
||||||
INVENTREE_API_VERSION = 8
|
INVENTREE_API_VERSION = 9
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v9 -> 2021-08-09
|
||||||
|
- Adds "price_string" to part pricing serializers
|
||||||
|
|
||||||
v8 -> 2021-07-19
|
v8 -> 2021-07-19
|
||||||
- Refactors the API interface for SupplierPart and ManufacturerPart models
|
- Refactors the API interface for SupplierPart and ManufacturerPart models
|
||||||
- ManufacturerPart objects can no longer be created via the SupplierPart API endpoint
|
- ManufacturerPart objects can no longer be created via the SupplierPart API endpoint
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ from stock.models import StockLocation, StockItem
|
|||||||
from common.models import InvenTreeSetting, ColorTheme
|
from common.models import InvenTreeSetting, ColorTheme
|
||||||
from users.models import check_user_role, RuleSet
|
from users.models import check_user_role, RuleSet
|
||||||
|
|
||||||
import InvenTree.tasks
|
|
||||||
|
|
||||||
from .forms import DeleteForm, EditUserForm, SetPasswordForm
|
from .forms import DeleteForm, EditUserForm, SetPasswordForm
|
||||||
from .forms import SettingCategorySelectForm
|
from .forms import SettingCategorySelectForm
|
||||||
from .helpers import str2bool
|
from .helpers import str2bool
|
||||||
@@ -827,8 +825,13 @@ class CurrencyRefreshView(RedirectView):
|
|||||||
On a POST request we will attempt to refresh the exchange rates
|
On a POST request we will attempt to refresh the exchange rates
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Will block for a little bit
|
from InvenTree.tasks import offload_task
|
||||||
InvenTree.tasks.update_exchange_rates()
|
|
||||||
|
# Define associated task from InvenTree.tasks list of methods
|
||||||
|
taskname = 'InvenTree.tasks.update_exchange_rates'
|
||||||
|
|
||||||
|
# Run it
|
||||||
|
offload_task(taskname, force_sync=True)
|
||||||
|
|
||||||
return redirect(reverse_lazy('settings'))
|
return redirect(reverse_lazy('settings'))
|
||||||
|
|
||||||
|
|||||||
@@ -926,6 +926,20 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"LABEL_INLINE": {
|
||||||
|
'name': _('Inline label display'),
|
||||||
|
'description': _('Display PDF labels in the browser, instead of downloading as a file'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
"REPORT_INLINE": {
|
||||||
|
'name': _('Inline report display'),
|
||||||
|
'description': _('Display PDF reports in the browser, instead of downloading as a file'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
|
||||||
'SEARCH_PREVIEW_RESULTS': {
|
'SEARCH_PREVIEW_RESULTS': {
|
||||||
'name': _('Search Preview Results'),
|
'name': _('Search Preview Results'),
|
||||||
'description': _('Number of results to show in search preview window'),
|
'description': _('Number of results to show in search preview window'),
|
||||||
@@ -965,7 +979,10 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_filters(cls, key, **kwargs):
|
def get_filters(cls, key, **kwargs):
|
||||||
return {'key__iexact': key, 'user__id': kwargs['user'].id}
|
return {
|
||||||
|
'key__iexact': key,
|
||||||
|
'user__id': kwargs['user'].id
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PriceBreak(models.Model):
|
class PriceBreak(models.Model):
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import os
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.db.models import Sum, Q, UniqueConstraint
|
from django.db.models import Sum, Q, UniqueConstraint
|
||||||
@@ -475,6 +476,32 @@ class SupplierPart(models.Model):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('supplier-part-detail', kwargs={'pk': self.id})
|
return reverse('supplier-part-detail', kwargs={'pk': self.id})
|
||||||
|
|
||||||
|
def api_instance_filters(self):
|
||||||
|
|
||||||
|
return {
|
||||||
|
'manufacturer_part': {
|
||||||
|
'part': self.part.pk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('part', 'supplier', 'SKU')
|
||||||
|
|
||||||
|
# This model was moved from the 'Part' app
|
||||||
|
db_table = 'part_supplierpart'
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Ensure that the linked manufacturer_part points to the same part!
|
||||||
|
if self.manufacturer_part and self.part:
|
||||||
|
|
||||||
|
if not self.manufacturer_part.part == self.part:
|
||||||
|
raise ValidationError({
|
||||||
|
'manufacturer_part': _("Linked manufacturer part must reference the same base part"),
|
||||||
|
})
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
""" Overriding save method to process the linked ManufacturerPart
|
""" Overriding save method to process the linked ManufacturerPart
|
||||||
"""
|
"""
|
||||||
@@ -526,12 +553,6 @@ class SupplierPart(models.Model):
|
|||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
unique_together = ('part', 'supplier', 'SKU')
|
|
||||||
|
|
||||||
# This model was moved from the 'Part' app
|
|
||||||
db_table = 'part_supplierpart'
|
|
||||||
|
|
||||||
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
|
part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
|
||||||
related_name='supplier_parts',
|
related_name='supplier_parts',
|
||||||
verbose_name=_('Base Part'),
|
verbose_name=_('Base Part'),
|
||||||
|
|||||||
@@ -109,10 +109,13 @@ class LabelPrintMixin:
|
|||||||
else:
|
else:
|
||||||
pdf = outputs[0].get_document().write_pdf()
|
pdf = outputs[0].get_document().write_pdf()
|
||||||
|
|
||||||
|
inline = common.models.InvenTreeUserSetting.get_setting('LABEL_INLINE', user=request.user)
|
||||||
|
|
||||||
return InvenTree.helpers.DownloadFile(
|
return InvenTree.helpers.DownloadFile(
|
||||||
pdf,
|
pdf,
|
||||||
label_name,
|
label_name,
|
||||||
content_type='application/pdf'
|
content_type='application/pdf',
|
||||||
|
inline=inline
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1860
-1702
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1811
-1653
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1872
-1714
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1939
-1781
File diff suppressed because it is too large
Load Diff
+1892
-1734
File diff suppressed because it is too large
Load Diff
+1827
-1669
File diff suppressed because it is too large
Load Diff
+1841
-1683
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1959
-1801
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
+1812
-1654
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.2.4 on 2021-08-07 11:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import part.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('part', '0070_alter_part_variant_of'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='partparametertemplate',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(help_text='Parameter Name', max_length=100, unique=True, validators=[part.models.validate_template_name], verbose_name='Name'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2143,6 +2143,16 @@ class PartTestTemplate(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_template_name(name):
|
||||||
|
"""
|
||||||
|
Prevent illegal characters in "name" field for PartParameterTemplate
|
||||||
|
"""
|
||||||
|
|
||||||
|
for c in "!@#$%^&*()<>{}[].,?/\|~`_+-=\'\"":
|
||||||
|
if c in str(name):
|
||||||
|
raise ValidationError(_(f"Illegal character in template name ({c})"))
|
||||||
|
|
||||||
|
|
||||||
class PartParameterTemplate(models.Model):
|
class PartParameterTemplate(models.Model):
|
||||||
"""
|
"""
|
||||||
A PartParameterTemplate provides a template for key:value pairs for extra
|
A PartParameterTemplate provides a template for key:value pairs for extra
|
||||||
@@ -2181,7 +2191,15 @@ class PartParameterTemplate(models.Model):
|
|||||||
except PartParameterTemplate.DoesNotExist:
|
except PartParameterTemplate.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
name = models.CharField(max_length=100, verbose_name=_('Name'), help_text=_('Parameter Name'), unique=True)
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
verbose_name=_('Name'),
|
||||||
|
help_text=_('Parameter Name'),
|
||||||
|
unique=True,
|
||||||
|
validators=[
|
||||||
|
validate_template_name,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
units = models.CharField(max_length=25, verbose_name=_('Units'), help_text=_('Parameter Units'), blank=True)
|
units = models.CharField(max_length=25, verbose_name=_('Units'), help_text=_('Parameter Units'), blank=True)
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ from djmoney.contrib.django_rest_framework import MoneyField
|
|||||||
|
|
||||||
from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
|
from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
|
||||||
InvenTreeImageSerializerField,
|
InvenTreeImageSerializerField,
|
||||||
InvenTreeModelSerializer)
|
InvenTreeModelSerializer,
|
||||||
|
InvenTreeMoneySerializer)
|
||||||
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
|
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
|
||||||
from stock.models import StockItem
|
from stock.models import StockItem
|
||||||
|
|
||||||
@@ -102,7 +103,12 @@ class PartSalePriceSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
quantity = serializers.FloatField()
|
quantity = serializers.FloatField()
|
||||||
|
|
||||||
price = serializers.CharField()
|
price = InvenTreeMoneySerializer(
|
||||||
|
max_digits=19, decimal_places=4,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
price_string = serializers.CharField(source='price', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PartSellPriceBreak
|
model = PartSellPriceBreak
|
||||||
@@ -111,6 +117,7 @@ class PartSalePriceSerializer(InvenTreeModelSerializer):
|
|||||||
'part',
|
'part',
|
||||||
'quantity',
|
'quantity',
|
||||||
'price',
|
'price',
|
||||||
|
'price_string',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -121,7 +128,12 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
quantity = serializers.FloatField()
|
quantity = serializers.FloatField()
|
||||||
|
|
||||||
price = serializers.CharField()
|
price = InvenTreeMoneySerializer(
|
||||||
|
max_digits=19, decimal_places=4,
|
||||||
|
allow_null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
price_string = serializers.CharField(source='price', read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PartInternalPriceBreak
|
model = PartInternalPriceBreak
|
||||||
@@ -130,6 +142,7 @@ class PartInternalPriceSerializer(InvenTreeModelSerializer):
|
|||||||
'part',
|
'part',
|
||||||
'quantity',
|
'quantity',
|
||||||
'price',
|
'price',
|
||||||
|
'price_string',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{% block form_alert %}
|
{% block form_alert %}
|
||||||
{% if missing_columns and missing_columns|length > 0 %}
|
{% if missing_columns and missing_columns|length > 0 %}
|
||||||
<div class='alert alert-danger alert-block' role='alert'>
|
<div class='alert alert-danger alert-block' style='margin-top:12px;' role='alert'>
|
||||||
{% trans "Missing selections for the following required columns" %}:
|
{% trans "Missing selections for the following required columns" %}:
|
||||||
<br>
|
<br>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -1,25 +1,35 @@
|
|||||||
{% extends "part/part_base.html" %}
|
{% extends "part/part_base.html" %}
|
||||||
{% load static %}
|
|
||||||
{% load i18n %}
|
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
{% block heading %}
|
{% block menubar %}
|
||||||
{% trans "Upload BOM File" %}
|
<ul class='list-group'>
|
||||||
|
<li class='list-group-item'>
|
||||||
|
<a href='#' id='part-menu-toggle'>
|
||||||
|
<span class='menu-tab-icon fas fa-expand-arrows-alt'></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class='list-group-item' title='{% trans "Return To BOM" %}'>
|
||||||
|
<a href='{% url "part-detail" part.id %}' id='select-upload-file' class='nav-toggle'>
|
||||||
|
<span class='fas fa-undo side-icon'></span>
|
||||||
|
{% trans "Return To BOM" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page_content %}
|
{% block page_content %}
|
||||||
|
|
||||||
|
<div class='panel panel-default panel-inventree' id='panel-upload-file'>
|
||||||
|
<div class='panel-heading'>
|
||||||
|
{% block heading %}
|
||||||
<h4>{% trans "Upload Bill of Materials" %}</h4>
|
<h4>{% trans "Upload Bill of Materials" %}</h4>
|
||||||
|
{{ wizard.form.media }}
|
||||||
{% block form_alert %}
|
|
||||||
<div class='alert alert-info alert-block'>
|
|
||||||
<b>{% trans "Requirements for BOM upload" %}:</b>
|
|
||||||
<ul>
|
|
||||||
<li>{% trans "The BOM file must contain the required named columns as provided in the " %} <b><a href="/part/bom_template/">{% trans "BOM Upload Template" %}</a></b></li>
|
|
||||||
<li>{% trans "Each part must already exist in the database" %}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
<div class='panel-content'>
|
||||||
|
{% block details %}
|
||||||
|
|
||||||
<p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %}
|
<p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %}
|
||||||
{% if description %}- {{ description }}{% endif %}</p>
|
{% if description %}- {{ description }}{% endif %}</p>
|
||||||
@@ -31,6 +41,16 @@
|
|||||||
{% block form_buttons_top %}
|
{% block form_buttons_top %}
|
||||||
{% endblock form_buttons_top %}
|
{% endblock form_buttons_top %}
|
||||||
|
|
||||||
|
{% block form_alert %}
|
||||||
|
<div class='alert alert-info alert-block'>
|
||||||
|
<b>{% trans "Requirements for BOM upload" %}:</b>
|
||||||
|
<ul>
|
||||||
|
<li>{% trans "The BOM file must contain the required named columns as provided in the " %} <b><a href="/part/bom_template/">{% trans "BOM Upload Template" %}</a></b></li>
|
||||||
|
<li>{% trans "Each part must already exist in the database" %}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<table class='table table-striped' style='margin-top: 12px; margin-bottom: 0px'>
|
<table class='table table-striped' style='margin-top: 12px; margin-bottom: 0px'>
|
||||||
{{ wizard.management_form }}
|
{{ wizard.management_form }}
|
||||||
{% block form_content %}
|
{% block form_content %}
|
||||||
@@ -46,4 +66,11 @@
|
|||||||
</form>
|
</form>
|
||||||
{% endblock form_buttons_bottom %}
|
{% endblock form_buttons_bottom %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock details %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock page_content %}
|
||||||
|
|
||||||
|
{% block js_ready %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock js_ready %}
|
||||||
|
|||||||
@@ -333,11 +333,111 @@
|
|||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
enableNavbar({
|
// Load the "suppliers" tab
|
||||||
label: 'part',
|
onPanelLoad('suppliers', function() {
|
||||||
toggleId: '#part-menu-toggle',
|
function reloadSupplierPartTable() {
|
||||||
|
$('#supplier-part-table').bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#supplier-create').click(function () {
|
||||||
|
|
||||||
|
createSupplierPart({
|
||||||
|
part: {{ part.pk }},
|
||||||
|
onSuccess: reloadSupplierPartTable,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#supplier-part-delete").click(function() {
|
||||||
|
|
||||||
|
var selections = $("#supplier-part-table").bootstrapTable("getSelections");
|
||||||
|
|
||||||
|
var requests = [];
|
||||||
|
|
||||||
|
showQuestionDialog(
|
||||||
|
'{% trans "Delete Supplier Parts?" %}',
|
||||||
|
'{% trans "All selected supplier parts will be deleted" %}',
|
||||||
|
{
|
||||||
|
accept: function() {
|
||||||
|
selections.forEach(function(part) {
|
||||||
|
var url = `/api/company/part/${part.pk}/`;
|
||||||
|
|
||||||
|
requests.push(inventreeDelete(url));
|
||||||
|
});
|
||||||
|
|
||||||
|
$.when.apply($, requests).done(function() {
|
||||||
|
reloadSupplierPartTable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
loadSupplierPartTable(
|
||||||
|
"#supplier-part-table",
|
||||||
|
"{% url 'api-supplier-part-list' %}",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
part: {{ part.id }},
|
||||||
|
part_detail: false,
|
||||||
|
supplier_detail: true,
|
||||||
|
manufacturer_detail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
linkButtonsToSelection($("#supplier-part-table"), ['#supplier-part-options']);
|
||||||
|
|
||||||
|
loadManufacturerPartTable(
|
||||||
|
'#manufacturer-part-table',
|
||||||
|
"{% url 'api-manufacturer-part-list' %}",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
part: {{ part.id }},
|
||||||
|
part_detail: true,
|
||||||
|
manufacturer_detail: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
linkButtonsToSelection($("#manufacturer-part-table"), ['#manufacturer-part-options']);
|
||||||
|
|
||||||
|
$("#manufacturer-part-delete").click(function() {
|
||||||
|
|
||||||
|
var selections = $("#manufacturer-part-table").bootstrapTable("getSelections");
|
||||||
|
|
||||||
|
deleteManufacturerParts(selections, {
|
||||||
|
onSuccess: function() {
|
||||||
|
$("#manufacturer-part-table").bootstrapTable("refresh");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#manufacturer-create').click(function () {
|
||||||
|
|
||||||
|
createManufacturerPart({
|
||||||
|
part: {{ part.pk }},
|
||||||
|
onSuccess: function() {
|
||||||
|
$("#manufacturer-part-table").bootstrapTable("refresh");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load the "builds" tab
|
||||||
|
onPanelLoad("build-orders", function() {
|
||||||
|
|
||||||
|
$("#start-build").click(function() {
|
||||||
|
newBuildOrder({
|
||||||
|
part: {{ part.pk }},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
loadBuildTable($("#build-table"), {
|
||||||
|
url: "{% url 'api-build-list' %}",
|
||||||
|
params: {
|
||||||
|
part: {{ part.id }},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
loadBuildOrderAllocationTable("#build-order-allocation-table", {
|
loadBuildOrderAllocationTable("#build-order-allocation-table", {
|
||||||
params: {
|
params: {
|
||||||
@@ -345,12 +445,19 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load the "sales orders" tab
|
||||||
|
onPanelLoad("sales-orders", function() {
|
||||||
loadSalesOrderAllocationTable("#sales-order-allocation-table", {
|
loadSalesOrderAllocationTable("#sales-order-allocation-table", {
|
||||||
params: {
|
params: {
|
||||||
part: {{ part.id }},
|
part: {{ part.id }},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load the "used in" tab
|
||||||
|
onPanelLoad("used-in", function() {
|
||||||
loadPartTable('#used-table',
|
loadPartTable('#used-table',
|
||||||
'{% url "api-part-list" %}',
|
'{% url "api-part-list" %}',
|
||||||
{
|
{
|
||||||
@@ -360,7 +467,10 @@
|
|||||||
filterTarget: '#filter-list-usedin',
|
filterTarget: '#filter-list-usedin',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load the "BOM" tab
|
||||||
|
onPanelLoad("bom", function() {
|
||||||
// Load the BOM table data
|
// Load the BOM table data
|
||||||
loadBomTable($("#bom-table"), {
|
loadBomTable($("#bom-table"), {
|
||||||
editable: {{ editing_enabled }},
|
editable: {{ editing_enabled }},
|
||||||
@@ -370,16 +480,6 @@
|
|||||||
sub_part_detail: true,
|
sub_part_detail: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load the BOM table data in the pricing view
|
|
||||||
loadBomTable($("#bom-pricing-table"), {
|
|
||||||
editable: {{ editing_enabled }},
|
|
||||||
bom_url: "{% url 'api-bom-list' %}",
|
|
||||||
part_url: "{% url 'api-part-list' %}",
|
|
||||||
parent_id: {{ part.id }} ,
|
|
||||||
sub_part_detail: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
linkButtonsToSelection($("#bom-table"),
|
linkButtonsToSelection($("#bom-table"),
|
||||||
[
|
[
|
||||||
"#bom-item-delete",
|
"#bom-item-delete",
|
||||||
@@ -488,20 +588,10 @@
|
|||||||
$("#print-bom-report").click(function() {
|
$("#print-bom-report").click(function() {
|
||||||
printBomReports([{{ part.pk }}]);
|
printBomReports([{{ part.pk }}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#start-build").click(function() {
|
|
||||||
newBuildOrder({
|
|
||||||
part: {{ part.pk }},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
loadBuildTable($("#build-table"), {
|
|
||||||
url: "{% url 'api-build-list' %}",
|
|
||||||
params: {
|
|
||||||
part: {{ part.id }},
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load the "related parts" tab
|
||||||
|
onPanelLoad("related-parts", function() {
|
||||||
$('#table-related-part').inventreeTable({
|
$('#table-related-part').inventreeTable({
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -521,7 +611,10 @@
|
|||||||
reload: true,
|
reload: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load the "variants" tab
|
||||||
|
onPanelLoad("variants", function() {
|
||||||
loadPartVariantTable($('#variants-table'), {{ part.pk }});
|
loadPartVariantTable($('#variants-table'), {{ part.pk }});
|
||||||
|
|
||||||
$('#new-variant').click(function() {
|
$('#new-variant').click(function() {
|
||||||
@@ -533,13 +626,36 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Load the BOM table data in the pricing view
|
||||||
|
loadBomTable($("#bom-pricing-table"), {
|
||||||
|
editable: {{ editing_enabled }},
|
||||||
|
bom_url: "{% url 'api-bom-list' %}",
|
||||||
|
part_url: "{% url 'api-part-list' %}",
|
||||||
|
parent_id: {{ part.id }} ,
|
||||||
|
sub_part_detail: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
onPanelLoad("purchase-orders", function() {
|
||||||
loadPurchaseOrderTable($("#purchase-order-table"), {
|
loadPurchaseOrderTable($("#purchase-order-table"), {
|
||||||
url: "{% url 'api-po-list' %}",
|
url: "{% url 'api-po-list' %}",
|
||||||
params: {
|
params: {
|
||||||
part: {{ part.id }},
|
part: {{ part.id }},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onPanelLoad("sales-orders", function() {
|
||||||
|
loadSalesOrderTable($("#sales-order-table"), {
|
||||||
|
url: "{% url 'api-so-list' %}",
|
||||||
|
params: {
|
||||||
|
part: {{ part.id }},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
$("#part-order2").click(function() {
|
$("#part-order2").click(function() {
|
||||||
launchModalForm("{% url 'order-parts' %}", {
|
launchModalForm("{% url 'order-parts' %}", {
|
||||||
@@ -550,13 +666,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
loadSalesOrderTable($("#sales-order-table"), {
|
onPanelLoad("test-templates", function() {
|
||||||
url: "{% url 'api-so-list' %}",
|
|
||||||
params: {
|
|
||||||
part: {{ part.id }},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
loadPartTestTemplateTable(
|
loadPartTestTemplateTable(
|
||||||
$("#test-template-table"),
|
$("#test-template-table"),
|
||||||
{
|
{
|
||||||
@@ -567,12 +677,12 @@
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function reloadTable() {
|
$("#add-test-template").click(function() {
|
||||||
|
|
||||||
|
function reloadTestTemplateTable() {
|
||||||
$("#test-template-table").bootstrapTable("refresh");
|
$("#test-template-table").bootstrapTable("refresh");
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#add-test-template").click(function() {
|
|
||||||
|
|
||||||
constructForm('{% url "api-part-test-template-list" %}', {
|
constructForm('{% url "api-part-test-template-list" %}', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
fields: {
|
fields: {
|
||||||
@@ -587,8 +697,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
title: '{% trans "Add Test Result Template" %}',
|
title: '{% trans "Add Test Result Template" %}',
|
||||||
onSuccess: reloadTable
|
onSuccess: reloadTestTemplateTable
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#test-template-table").on('click', '.button-test-edit', function() {
|
$("#test-template-table").on('click', '.button-test-edit', function() {
|
||||||
@@ -605,7 +714,7 @@
|
|||||||
requires_attachment: {},
|
requires_attachment: {},
|
||||||
},
|
},
|
||||||
title: '{% trans "Edit Test Result Template" %}',
|
title: '{% trans "Edit Test Result Template" %}',
|
||||||
onSuccess: reloadTable,
|
onSuccess: reloadTestTemplateTable,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -617,10 +726,14 @@
|
|||||||
constructForm(url, {
|
constructForm(url, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
title: '{% trans "Delete Test Result Template" %}',
|
title: '{% trans "Delete Test Result Template" %}',
|
||||||
onSuccess: reloadTable,
|
onSuccess: reloadTestTemplateTable,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onPanelLoad("part-stock", function() {
|
||||||
$('#add-stock-item').click(function () {
|
$('#add-stock-item').click(function () {
|
||||||
createNewStockItem({
|
createNewStockItem({
|
||||||
reload: true,
|
reload: true,
|
||||||
@@ -659,6 +772,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
$('#edit-notes').click(function() {
|
$('#edit-notes').click(function() {
|
||||||
constructForm('{% url "api-part-detail" part.pk %}', {
|
constructForm('{% url "api-part-detail" part.pk %}', {
|
||||||
@@ -690,6 +804,7 @@
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onPanelLoad("part-parameters", function() {
|
||||||
loadPartParameterTable(
|
loadPartParameterTable(
|
||||||
'#parameter-table',
|
'#parameter-table',
|
||||||
'{% url "api-part-parameter-list" %}',
|
'{% url "api-part-parameter-list" %}',
|
||||||
@@ -739,7 +854,9 @@
|
|||||||
reload: true,
|
reload: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onPanelLoad("part-attachments", function() {
|
||||||
loadAttachmentTable(
|
loadAttachmentTable(
|
||||||
'{% url "api-part-attachment-list" %}',
|
'{% url "api-part-attachment-list" %}',
|
||||||
{
|
{
|
||||||
@@ -803,93 +920,8 @@
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
function reloadSupplierPartTable() {
|
|
||||||
$('#supplier-part-table').bootstrapTable('refresh');
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#supplier-create').click(function () {
|
|
||||||
|
|
||||||
createSupplierPart({
|
|
||||||
part: {{ part.pk }},
|
|
||||||
onSuccess: reloadSupplierPartTable,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#supplier-part-delete").click(function() {
|
|
||||||
|
|
||||||
var selections = $("#supplier-part-table").bootstrapTable("getSelections");
|
|
||||||
|
|
||||||
var requests = [];
|
|
||||||
|
|
||||||
showQuestionDialog(
|
|
||||||
'{% trans "Delete Supplier Parts?" %}',
|
|
||||||
'{% trans "All selected supplier parts will be deleted" %}',
|
|
||||||
{
|
|
||||||
accept: function() {
|
|
||||||
selections.forEach(function(part) {
|
|
||||||
var url = `/api/company/part/${part.pk}/`;
|
|
||||||
|
|
||||||
requests.push(inventreeDelete(url));
|
|
||||||
});
|
|
||||||
|
|
||||||
$.when.apply($, requests).done(function() {
|
|
||||||
reloadSupplierPartTable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
loadSupplierPartTable(
|
|
||||||
"#supplier-part-table",
|
|
||||||
"{% url 'api-supplier-part-list' %}",
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
part: {{ part.id }},
|
|
||||||
part_detail: false,
|
|
||||||
supplier_detail: true,
|
|
||||||
manufacturer_detail: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
linkButtonsToSelection($("#supplier-part-table"), ['#supplier-part-options']);
|
|
||||||
|
|
||||||
loadManufacturerPartTable(
|
|
||||||
'#manufacturer-part-table',
|
|
||||||
"{% url 'api-manufacturer-part-list' %}",
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
part: {{ part.id }},
|
|
||||||
part_detail: true,
|
|
||||||
manufacturer_detail: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
linkButtonsToSelection($("#manufacturer-part-table"), ['#manufacturer-part-options']);
|
|
||||||
|
|
||||||
$("#manufacturer-part-delete").click(function() {
|
|
||||||
|
|
||||||
var selections = $("#manufacturer-part-table").bootstrapTable("getSelections");
|
|
||||||
|
|
||||||
deleteManufacturerParts(selections, {
|
|
||||||
onSuccess: function() {
|
|
||||||
$("#manufacturer-part-table").bootstrapTable("refresh");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#manufacturer-create').click(function () {
|
|
||||||
|
|
||||||
createManufacturerPart({
|
|
||||||
part: {{ part.pk }},
|
|
||||||
onSuccess: function() {
|
|
||||||
$("#manufacturer-part-table").bootstrapTable("refresh");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
{% default_currency as currency %}
|
{% default_currency as currency %}
|
||||||
|
|
||||||
|
|||||||
@@ -213,6 +213,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<!-- Details show/hide button -->
|
<!-- Details show/hide button -->
|
||||||
<button id="toggle-part-details" class="btn btn-primary" data-toggle="collapse" data-target="#collapsible-part-details" value="show">
|
<button id="toggle-part-details" class="btn btn-primary" data-toggle="collapse" data-target="#collapsible-part-details" value="show">
|
||||||
|
<span class="fas fa-chevron-down"></span> {% trans "Show Part Details" %}
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -305,6 +306,11 @@
|
|||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
|
enableNavbar({
|
||||||
|
label: 'part',
|
||||||
|
toggleId: '#part-menu-toggle',
|
||||||
|
});
|
||||||
|
|
||||||
{% if part.image %}
|
{% if part.image %}
|
||||||
$('#part-thumb').click(function() {
|
$('#part-thumb').click(function() {
|
||||||
showModalImage('{{ part.image.url }}');
|
showModalImage('{{ part.image.url }}');
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ def settings_value(key, *args, **kwargs):
|
|||||||
|
|
||||||
if 'user' in kwargs:
|
if 'user' in kwargs:
|
||||||
return InvenTreeUserSetting.get_setting(key, user=kwargs['user'])
|
return InvenTreeUserSetting.get_setting(key, user=kwargs['user'])
|
||||||
|
|
||||||
return InvenTreeSetting.get_setting(key)
|
return InvenTreeSetting.get_setting(key)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -254,10 +254,13 @@ class ReportPrintMixin:
|
|||||||
else:
|
else:
|
||||||
pdf = outputs[0].get_document().write_pdf()
|
pdf = outputs[0].get_document().write_pdf()
|
||||||
|
|
||||||
|
inline = common.models.InvenTreeUserSetting.get_setting('REPORT_INLINE', user=request.user)
|
||||||
|
|
||||||
return InvenTree.helpers.DownloadFile(
|
return InvenTree.helpers.DownloadFile(
|
||||||
pdf,
|
pdf,
|
||||||
report_name,
|
report_name,
|
||||||
content_type='application/pdf'
|
content_type='application/pdf',
|
||||||
|
inline=inline,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load l10n %}
|
||||||
{% load markdownify %}
|
{% load markdownify %}
|
||||||
|
|
||||||
{% block menubar %}
|
{% block menubar %}
|
||||||
@@ -152,7 +153,7 @@
|
|||||||
{
|
{
|
||||||
stock_item: {{ item.pk }},
|
stock_item: {{ item.pk }},
|
||||||
part: {{ item.part.pk }},
|
part: {{ item.part.pk }},
|
||||||
quantity: {{ item.quantity }},
|
quantity: {{ item.quantity|unlocalize }},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,18 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class='list-group-item' title='{% trans "Labels" %}'>
|
||||||
|
<a href='#' class='nav-toggle' id='select-user-labels'>
|
||||||
|
<span class='fas fa-tag'></span> {% trans "Labels" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class='list-group-item' title='{% trans "Reports" %}'>
|
||||||
|
<a href='#' class='nav-toggle' id='select-user-reports'>
|
||||||
|
<span class='fas fa-file-pdf'></span> {% trans "Reports" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<li class='list-group-item' title='{% trans "Settings" %}'>
|
<li class='list-group-item' title='{% trans "Settings" %}'>
|
||||||
<a href='#' class='nav-toggle' id='select-user-settings'>
|
<a href='#' class='nav-toggle' id='select-user-settings'>
|
||||||
|
|||||||
@@ -68,80 +68,3 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_ready %}
|
|
||||||
{{ block.super }}
|
|
||||||
|
|
||||||
$("#param-table").inventreeTable({
|
|
||||||
url: "{% url 'api-part-parameter-template-list' %}",
|
|
||||||
queryParams: {
|
|
||||||
ordering: 'name',
|
|
||||||
},
|
|
||||||
formatNoMatches: function() { return '{% trans "No part parameter templates found" %}'; },
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'pk',
|
|
||||||
title: 'ID',
|
|
||||||
visible: false,
|
|
||||||
switchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'name',
|
|
||||||
title: 'Name',
|
|
||||||
sortable: 'true',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'units',
|
|
||||||
title: 'Units',
|
|
||||||
sortable: 'true',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
formatter: function(value, row, index, field) {
|
|
||||||
var bEdit = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-edit'></span></button>";
|
|
||||||
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
|
|
||||||
|
|
||||||
var html = "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#new-param").click(function() {
|
|
||||||
launchModalForm("{% url 'part-param-template-create' %}", {
|
|
||||||
success: function() {
|
|
||||||
$("#param-table").bootstrapTable('refresh');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#param-table").on('click', '.template-edit', function() {
|
|
||||||
var button = $(this);
|
|
||||||
|
|
||||||
var url = "/part/parameter/template/" + button.attr('pk') + "/edit/";
|
|
||||||
|
|
||||||
launchModalForm(url, {
|
|
||||||
success: function() {
|
|
||||||
$("#param-table").bootstrapTable('refresh');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#param-table").on('click', '.template-delete', function() {
|
|
||||||
var button = $(this);
|
|
||||||
|
|
||||||
var url = "/part/parameter/template/" + button.attr('pk') + "/delete/";
|
|
||||||
|
|
||||||
launchModalForm(url, {
|
|
||||||
success: function() {
|
|
||||||
$("#param-table").bootstrapTable('refresh');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#import-part").click(function() {
|
|
||||||
launchModalForm("{% url 'api-part-import' %}?reset", {});
|
|
||||||
});
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -18,6 +18,8 @@
|
|||||||
{% include "InvenTree/settings/user_settings.html" %}
|
{% include "InvenTree/settings/user_settings.html" %}
|
||||||
{% include "InvenTree/settings/user_homepage.html" %}
|
{% include "InvenTree/settings/user_homepage.html" %}
|
||||||
{% include "InvenTree/settings/user_search.html" %}
|
{% include "InvenTree/settings/user_search.html" %}
|
||||||
|
{% include "InvenTree/settings/user_labels.html" %}
|
||||||
|
{% include "InvenTree/settings/user_reports.html" %}
|
||||||
|
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
|
|
||||||
@@ -241,6 +243,79 @@ $("#cat-param-table").on('click', '.template-delete', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#param-table").inventreeTable({
|
||||||
|
url: "{% url 'api-part-parameter-template-list' %}",
|
||||||
|
queryParams: {
|
||||||
|
ordering: 'name',
|
||||||
|
},
|
||||||
|
formatNoMatches: function() { return '{% trans "No part parameter templates found" %}'; },
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'pk',
|
||||||
|
title: 'ID',
|
||||||
|
visible: false,
|
||||||
|
switchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: 'Name',
|
||||||
|
sortable: 'true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'units',
|
||||||
|
title: 'Units',
|
||||||
|
sortable: 'true',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
formatter: function(value, row, index, field) {
|
||||||
|
var bEdit = "<button title='{% trans "Edit Template" %}' class='template-edit btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-edit'></span></button>";
|
||||||
|
var bDel = "<button title='{% trans "Delete Template" %}' class='template-delete btn btn-default btn-glyph' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
|
||||||
|
|
||||||
|
var html = "<div class='btn-group float-right' role='group'>" + bEdit + bDel + "</div>";
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#new-param").click(function() {
|
||||||
|
launchModalForm("{% url 'part-param-template-create' %}", {
|
||||||
|
success: function() {
|
||||||
|
$("#param-table").bootstrapTable('refresh');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#param-table").on('click', '.template-edit', function() {
|
||||||
|
var button = $(this);
|
||||||
|
|
||||||
|
var url = "/part/parameter/template/" + button.attr('pk') + "/edit/";
|
||||||
|
|
||||||
|
launchModalForm(url, {
|
||||||
|
success: function() {
|
||||||
|
$("#param-table").bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#param-table").on('click', '.template-delete', function() {
|
||||||
|
var button = $(this);
|
||||||
|
|
||||||
|
var url = "/part/parameter/template/" + button.attr('pk') + "/delete/";
|
||||||
|
|
||||||
|
launchModalForm(url, {
|
||||||
|
success: function() {
|
||||||
|
$("#param-table").bootstrapTable('refresh');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#import-part").click(function() {
|
||||||
|
launchModalForm("{% url 'api-part-import' %}?reset", {});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
enableNavbar({
|
enableNavbar({
|
||||||
label: 'settings',
|
label: 'settings',
|
||||||
toggleId: '#item-menu-toggle',
|
toggleId: '#item-menu-toggle',
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends "panel.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
|
{% block label %}user-labels{% endblock %}
|
||||||
|
|
||||||
|
{% block heading %}
|
||||||
|
{% trans "Label Settings" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class='row'>
|
||||||
|
<table class='table table-striped table-condensed'>
|
||||||
|
{% include "InvenTree/settings/header.html" %}
|
||||||
|
<tbody>
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="LABEL_INLINE" icon='fa-tag' user_setting=True %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{% extends "panel.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
|
{% block label %}user-reports{% endblock %}
|
||||||
|
|
||||||
|
{% block heading %}
|
||||||
|
{% trans "Report Settings" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class='row'>
|
||||||
|
<table class='table table-striped table-condensed'>
|
||||||
|
{% include "InvenTree/settings/header.html" %}
|
||||||
|
<tbody>
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="REPORT_INLINE" icon='fa-file-pdf' user_setting=True %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -72,7 +72,12 @@ function activatePanel(panelName, options={}) {
|
|||||||
|
|
||||||
// Display the panel
|
// Display the panel
|
||||||
$(panel).addClass('panel-visible');
|
$(panel).addClass('panel-visible');
|
||||||
$(panel).fadeIn(100);
|
|
||||||
|
// Load the data
|
||||||
|
$(panel).trigger('fadeInStarted');
|
||||||
|
|
||||||
|
$(panel).fadeIn(100, function() {
|
||||||
|
});
|
||||||
|
|
||||||
// Un-select all selectors
|
// Un-select all selectors
|
||||||
$('.list-group-item').removeClass('active');
|
$('.list-group-item').removeClass('active');
|
||||||
@@ -82,3 +87,22 @@ function activatePanel(panelName, options={}) {
|
|||||||
|
|
||||||
$(select).parent('.list-group-item').addClass('active');
|
$(select).parent('.list-group-item').addClass('active');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onPanelLoad(panel, callback) {
|
||||||
|
// One-time callback when a panel is first displayed
|
||||||
|
// Used to implement lazy-loading, rather than firing
|
||||||
|
// multiple AJAX queries when the page is first loaded.
|
||||||
|
|
||||||
|
var panelId = `#panel-${panel}`;
|
||||||
|
|
||||||
|
$(panelId).on('fadeInStarted', function(e) {
|
||||||
|
|
||||||
|
// Trigger the callback
|
||||||
|
callback();
|
||||||
|
|
||||||
|
// Turn off the event
|
||||||
|
$(panelId).off('fadeInStarted');
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -30,6 +30,17 @@ function createManufacturerPart(options={}) {
|
|||||||
fields.manufacturer.value = options.manufacturer;
|
fields.manufacturer.value = options.manufacturer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fields.manufacturer.secondary = {
|
||||||
|
title: '{% trans "Add Manufacturer" %}',
|
||||||
|
fields: function(data) {
|
||||||
|
var company_fields = companyFormFields();
|
||||||
|
|
||||||
|
company_fields.is_manufacturer.value = true;
|
||||||
|
|
||||||
|
return company_fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructForm('{% url "api-manufacturer-part-list" %}', {
|
constructForm('{% url "api-manufacturer-part-list" %}', {
|
||||||
fields: fields,
|
fields: fields,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -43,8 +54,12 @@ function editManufacturerPart(part, options={}) {
|
|||||||
|
|
||||||
var url = `/api/company/part/manufacturer/${part}/`;
|
var url = `/api/company/part/manufacturer/${part}/`;
|
||||||
|
|
||||||
|
var fields = manufacturerPartFields();
|
||||||
|
|
||||||
|
fields.part.hidden = true;
|
||||||
|
|
||||||
constructForm(url, {
|
constructForm(url, {
|
||||||
fields: manufacturerPartFields(),
|
fields: fields,
|
||||||
title: '{% trans "Edit Manufacturer Part" %}',
|
title: '{% trans "Edit Manufacturer Part" %}',
|
||||||
onSuccess: options.onSuccess
|
onSuccess: options.onSuccess
|
||||||
});
|
});
|
||||||
@@ -72,7 +87,7 @@ function supplierPartFields() {
|
|||||||
filters: {
|
filters: {
|
||||||
part_detail: true,
|
part_detail: true,
|
||||||
manufacturer_detail: true,
|
manufacturer_detail: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
description: {},
|
description: {},
|
||||||
link: {
|
link: {
|
||||||
@@ -108,6 +123,33 @@ function createSupplierPart(options={}) {
|
|||||||
fields.manufacturer_part.value = options.manufacturer_part;
|
fields.manufacturer_part.value = options.manufacturer_part;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a secondary modal for the supplier
|
||||||
|
fields.supplier.secondary = {
|
||||||
|
title: '{% trans "Add Supplier" %}',
|
||||||
|
fields: function(data) {
|
||||||
|
var company_fields = companyFormFields();
|
||||||
|
|
||||||
|
company_fields.is_supplier.value = true;
|
||||||
|
|
||||||
|
return company_fields;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add a secondary modal for the manufacturer part
|
||||||
|
fields.manufacturer_part.secondary = {
|
||||||
|
title: '{% trans "Add Manufacturer Part" %}',
|
||||||
|
fields: function(data) {
|
||||||
|
var mp_fields = manufacturerPartFields();
|
||||||
|
|
||||||
|
if (data.part) {
|
||||||
|
mp_fields.part.value = data.part;
|
||||||
|
mp_fields.part.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mp_fields;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
constructForm('{% url "api-supplier-part-list" %}', {
|
constructForm('{% url "api-supplier-part-list" %}', {
|
||||||
fields: fields,
|
fields: fields,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -119,8 +161,13 @@ function createSupplierPart(options={}) {
|
|||||||
|
|
||||||
function editSupplierPart(part, options={}) {
|
function editSupplierPart(part, options={}) {
|
||||||
|
|
||||||
|
var fields = supplierPartFields();
|
||||||
|
|
||||||
|
// Hide the "part" field
|
||||||
|
fields.part.hidden = true;
|
||||||
|
|
||||||
constructForm(`/api/company/part/${part}/`, {
|
constructForm(`/api/company/part/${part}/`, {
|
||||||
fields: supplierPartFields(),
|
fields: fields,
|
||||||
title: '{% trans "Edit Supplier Part" %}',
|
title: '{% trans "Edit Supplier Part" %}',
|
||||||
onSuccess: options.onSuccess
|
onSuccess: options.onSuccess
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -564,6 +564,30 @@ function insertConfirmButton(options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extract all specified form values as a single object
|
||||||
|
*/
|
||||||
|
function extractFormData(fields, options) {
|
||||||
|
|
||||||
|
var data = {};
|
||||||
|
|
||||||
|
for (var idx = 0; idx < options.field_names.length; idx++) {
|
||||||
|
|
||||||
|
var name = options.field_names[idx];
|
||||||
|
|
||||||
|
var field = fields[name] || null;
|
||||||
|
|
||||||
|
if (!field) continue;
|
||||||
|
|
||||||
|
if (field.type == 'candy') continue;
|
||||||
|
|
||||||
|
data[name] = getFormFieldValue(name, field, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Submit form data to the server.
|
* Submit form data to the server.
|
||||||
*
|
*
|
||||||
@@ -950,10 +974,10 @@ function initializeRelatedFields(fields, options) {
|
|||||||
|
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case 'related field':
|
case 'related field':
|
||||||
initializeRelatedField(name, field, options);
|
initializeRelatedField(field, fields, options);
|
||||||
break;
|
break;
|
||||||
case 'choice':
|
case 'choice':
|
||||||
initializeChoiceField(name, field, options);
|
initializeChoiceField(field, fields, options);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -968,7 +992,9 @@ function initializeRelatedFields(fields, options) {
|
|||||||
* - field: The field data object
|
* - field: The field data object
|
||||||
* - options: The options object provided by the client
|
* - options: The options object provided by the client
|
||||||
*/
|
*/
|
||||||
function addSecondaryModal(name, field, options) {
|
function addSecondaryModal(field, fields, options) {
|
||||||
|
|
||||||
|
var name = field.name;
|
||||||
|
|
||||||
var secondary = field.secondary;
|
var secondary = field.secondary;
|
||||||
|
|
||||||
@@ -981,22 +1007,42 @@ function addSecondaryModal(name, field, options) {
|
|||||||
|
|
||||||
$(options.modal).find(`label[for="id_${name}"]`).append(html);
|
$(options.modal).find(`label[for="id_${name}"]`).append(html);
|
||||||
|
|
||||||
// TODO: Launch a callback
|
// Callback function when the secondary button is pressed
|
||||||
$(options.modal).find(`#btn-new-${name}`).click(function() {
|
$(options.modal).find(`#btn-new-${name}`).click(function() {
|
||||||
|
|
||||||
if (secondary.callback) {
|
// Determine the API query URL
|
||||||
// A "custom" callback can be specified for the button
|
var url = secondary.api_url || field.api_url;
|
||||||
secondary.callback(field, options);
|
|
||||||
} else if (secondary.api_url) {
|
|
||||||
// By default, a new modal form is created, with the parameters specified
|
|
||||||
// The parameters match the "normal" form creation parameters
|
|
||||||
|
|
||||||
secondary.onSuccess = function(data, opts) {
|
// If the "fields" attribute is a function, call it with data
|
||||||
setRelatedFieldData(name, data, options);
|
if (secondary.fields instanceof Function) {
|
||||||
};
|
|
||||||
|
|
||||||
constructForm(secondary.api_url, secondary);
|
// Extract form values at time of button press
|
||||||
|
var data = extractFormData(fields, options)
|
||||||
|
|
||||||
|
secondary.fields = secondary.fields(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If no onSuccess function is defined, provide a default one
|
||||||
|
if (!secondary.onSuccess) {
|
||||||
|
secondary.onSuccess = function(data, opts) {
|
||||||
|
|
||||||
|
// Force refresh from the API, to get full detail
|
||||||
|
inventreeGet(`${url}${data.pk}/`, {}, {
|
||||||
|
success: function(responseData) {
|
||||||
|
|
||||||
|
setRelatedFieldData(name, responseData, options);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method should be "POST" for creation
|
||||||
|
secondary.method = secondary.method || 'POST';
|
||||||
|
|
||||||
|
constructForm(
|
||||||
|
url,
|
||||||
|
secondary
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1010,7 +1056,9 @@ function addSecondaryModal(name, field, options) {
|
|||||||
* - field: Field definition from the OPTIONS request
|
* - field: Field definition from the OPTIONS request
|
||||||
* - options: Original options object provided by the client
|
* - options: Original options object provided by the client
|
||||||
*/
|
*/
|
||||||
function initializeRelatedField(name, field, options) {
|
function initializeRelatedField(field, fields, options) {
|
||||||
|
|
||||||
|
var name = field.name;
|
||||||
|
|
||||||
if (!field.api_url) {
|
if (!field.api_url) {
|
||||||
// TODO: Provide manual api_url option?
|
// TODO: Provide manual api_url option?
|
||||||
@@ -1023,7 +1071,7 @@ function initializeRelatedField(name, field, options) {
|
|||||||
|
|
||||||
// Add a button to launch a 'secondary' modal
|
// Add a button to launch a 'secondary' modal
|
||||||
if (field.secondary != null) {
|
if (field.secondary != null) {
|
||||||
addSecondaryModal(name, field, options);
|
addSecondaryModal(field, fields, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add 'placeholder' support for entry select2 fields
|
// TODO: Add 'placeholder' support for entry select2 fields
|
||||||
@@ -1192,7 +1240,9 @@ function setRelatedFieldData(name, data, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function initializeChoiceField(name, field, options) {
|
function initializeChoiceField(field, fields, options) {
|
||||||
|
|
||||||
|
var name = field.name;
|
||||||
|
|
||||||
var select = $(options.modal).find(`#id_${name}`);
|
var select = $(options.modal).find(`#id_${name}`);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ function createSalesOrder(options={}) {
|
|||||||
},
|
},
|
||||||
customer: {
|
customer: {
|
||||||
value: options.customer,
|
value: options.customer,
|
||||||
|
secondary: {
|
||||||
|
title: '{% trans "Add Customer" %}',
|
||||||
|
fields: function(data) {
|
||||||
|
var fields = companyFormFields();
|
||||||
|
|
||||||
|
fields.is_customer.value = true;
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
customer_reference: {},
|
customer_reference: {},
|
||||||
description: {},
|
description: {},
|
||||||
@@ -44,6 +54,16 @@ function createPurchaseOrder(options={}) {
|
|||||||
},
|
},
|
||||||
supplier: {
|
supplier: {
|
||||||
value: options.supplier,
|
value: options.supplier,
|
||||||
|
secondary: {
|
||||||
|
title: '{% trans "Add Supplier" %}',
|
||||||
|
fields: function(data) {
|
||||||
|
var fields = companyFormFields();
|
||||||
|
|
||||||
|
fields.is_supplier.value = true;
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
supplier_reference: {},
|
supplier_reference: {},
|
||||||
description: {},
|
description: {},
|
||||||
|
|||||||
@@ -17,7 +17,16 @@ function yesNoLabel(value) {
|
|||||||
function partFields(options={}) {
|
function partFields(options={}) {
|
||||||
|
|
||||||
var fields = {
|
var fields = {
|
||||||
category: {},
|
category: {
|
||||||
|
secondary: {
|
||||||
|
title: '{% trans "Add Part Category" %}',
|
||||||
|
fields: function(data) {
|
||||||
|
var fields = categoryFields();
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
name: {},
|
name: {},
|
||||||
IPN: {},
|
IPN: {},
|
||||||
revision: {},
|
revision: {},
|
||||||
@@ -30,7 +39,8 @@ function partFields(options={}) {
|
|||||||
link: {
|
link: {
|
||||||
icon: 'fa-link',
|
icon: 'fa-link',
|
||||||
},
|
},
|
||||||
default_location: {},
|
default_location: {
|
||||||
|
},
|
||||||
default_supplier: {},
|
default_supplier: {},
|
||||||
default_expiry: {
|
default_expiry: {
|
||||||
icon: 'fa-calendar-alt',
|
icon: 'fa-calendar-alt',
|
||||||
@@ -1096,6 +1106,7 @@ function loadPriceBreakTable(table, options) {
|
|||||||
formatNoMatches: function() {
|
formatNoMatches: function() {
|
||||||
return `{% trans "No ${human_name} information found" %}`;
|
return `{% trans "No ${human_name} information found" %}`;
|
||||||
},
|
},
|
||||||
|
queryParams: {part: options.part},
|
||||||
url: options.url,
|
url: options.url,
|
||||||
onLoadSuccess: function(tableData) {
|
onLoadSuccess: function(tableData) {
|
||||||
if (linkedGraph) {
|
if (linkedGraph) {
|
||||||
@@ -1104,7 +1115,7 @@ function loadPriceBreakTable(table, options) {
|
|||||||
|
|
||||||
// split up for graph definition
|
// split up for graph definition
|
||||||
var graphLabels = Array.from(tableData, x => x.quantity);
|
var graphLabels = Array.from(tableData, x => x.quantity);
|
||||||
var graphData = Array.from(tableData, x => parseFloat(x.price));
|
var graphData = Array.from(tableData, x => x.price);
|
||||||
|
|
||||||
// destroy chart if exists
|
// destroy chart if exists
|
||||||
if (chart){
|
if (chart){
|
||||||
@@ -1191,6 +1202,7 @@ function initPriceBreakSet(table, options) {
|
|||||||
human_name: pb_human_name,
|
human_name: pb_human_name,
|
||||||
url: pb_url,
|
url: pb_url,
|
||||||
linkedGraph: linkedGraph,
|
linkedGraph: linkedGraph,
|
||||||
|
part: part_id,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,18 @@
|
|||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load status_codes %}
|
{% load status_codes %}
|
||||||
|
|
||||||
|
|
||||||
|
function locationFields() {
|
||||||
|
return {
|
||||||
|
parent: {
|
||||||
|
help_text: '{% trans "Parent stock location" %}',
|
||||||
|
},
|
||||||
|
name: {},
|
||||||
|
description: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Stock API functions
|
/* Stock API functions
|
||||||
* Requires api.js to be loaded first
|
* Requires api.js to be loaded first
|
||||||
*/
|
*/
|
||||||
@@ -251,11 +263,13 @@ function adjustStock(action, items, options={}) {
|
|||||||
required: true,
|
required: true,
|
||||||
api_url: `/api/stock/location/`,
|
api_url: `/api/stock/location/`,
|
||||||
model: 'stocklocation',
|
model: 'stocklocation',
|
||||||
|
name: 'location',
|
||||||
},
|
},
|
||||||
notes: {
|
notes: {
|
||||||
label: '{% trans "Notes" %}',
|
label: '{% trans "Notes" %}',
|
||||||
help_text: '{% trans "Stock transaction notes" %}',
|
help_text: '{% trans "Stock transaction notes" %}',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
name: 'notes',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -653,6 +667,7 @@ function loadStockTable(table, options) {
|
|||||||
// List of user-params which override the default filters
|
// List of user-params which override the default filters
|
||||||
|
|
||||||
options.params['location_detail'] = true;
|
options.params['location_detail'] = true;
|
||||||
|
options.params['part_detail'] = true;
|
||||||
|
|
||||||
var params = options.params || {};
|
var params = options.params || {};
|
||||||
|
|
||||||
@@ -1102,11 +1117,11 @@ function loadStockTable(table, options) {
|
|||||||
|
|
||||||
|
|
||||||
function stockAdjustment(action) {
|
function stockAdjustment(action) {
|
||||||
var items = $("#stock-table").bootstrapTable("getSelections");
|
var items = $(table).bootstrapTable("getSelections");
|
||||||
|
|
||||||
adjustStock(action, items, {
|
adjustStock(action, items, {
|
||||||
onSuccess: function() {
|
onSuccess: function() {
|
||||||
$('#stock-table').bootstrapTable('refresh');
|
$(table).bootstrapTable('refresh');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1114,7 +1129,7 @@ function loadStockTable(table, options) {
|
|||||||
// Automatically link button callbacks
|
// Automatically link button callbacks
|
||||||
|
|
||||||
$('#multi-item-print-label').click(function() {
|
$('#multi-item-print-label').click(function() {
|
||||||
var selections = $('#stock-table').bootstrapTable('getSelections');
|
var selections = $(table).bootstrapTable('getSelections');
|
||||||
|
|
||||||
var items = [];
|
var items = [];
|
||||||
|
|
||||||
@@ -1126,7 +1141,7 @@ function loadStockTable(table, options) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('#multi-item-print-test-report').click(function() {
|
$('#multi-item-print-test-report').click(function() {
|
||||||
var selections = $('#stock-table').bootstrapTable('getSelections');
|
var selections = $(table).bootstrapTable('getSelections');
|
||||||
|
|
||||||
var items = [];
|
var items = [];
|
||||||
|
|
||||||
@@ -1139,7 +1154,7 @@ function loadStockTable(table, options) {
|
|||||||
|
|
||||||
if (global_settings.BARCODE_ENABLE) {
|
if (global_settings.BARCODE_ENABLE) {
|
||||||
$('#multi-item-barcode-scan-into-location').click(function() {
|
$('#multi-item-barcode-scan-into-location').click(function() {
|
||||||
var selections = $('#stock-table').bootstrapTable('getSelections');
|
var selections = $(table).bootstrapTable('getSelections');
|
||||||
|
|
||||||
var items = [];
|
var items = [];
|
||||||
|
|
||||||
@@ -1168,7 +1183,7 @@ function loadStockTable(table, options) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#multi-item-order").click(function() {
|
$("#multi-item-order").click(function() {
|
||||||
var selections = $("#stock-table").bootstrapTable("getSelections");
|
var selections = $(table).bootstrapTable("getSelections");
|
||||||
|
|
||||||
var stock = [];
|
var stock = [];
|
||||||
|
|
||||||
@@ -1185,7 +1200,7 @@ function loadStockTable(table, options) {
|
|||||||
|
|
||||||
$("#multi-item-set-status").click(function() {
|
$("#multi-item-set-status").click(function() {
|
||||||
// Select and set the STATUS field for selected stock items
|
// Select and set the STATUS field for selected stock items
|
||||||
var selections = $("#stock-table").bootstrapTable('getSelections');
|
var selections = $(table).bootstrapTable('getSelections');
|
||||||
|
|
||||||
// Select stock status
|
// Select stock status
|
||||||
var modal = '#modal-form';
|
var modal = '#modal-form';
|
||||||
@@ -1265,13 +1280,13 @@ function loadStockTable(table, options) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$.when.apply($, requests).done(function() {
|
$.when.apply($, requests).done(function() {
|
||||||
$("#stock-table").bootstrapTable('refresh');
|
$(table).bootstrapTable('refresh');
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#multi-item-delete").click(function() {
|
$("#multi-item-delete").click(function() {
|
||||||
var selections = $("#stock-table").bootstrapTable("getSelections");
|
var selections = $(table).bootstrapTable("getSelections");
|
||||||
|
|
||||||
var stock = [];
|
var stock = [];
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ However, powerful business logic works in the background to ensure that stock tr
|
|||||||
|
|
||||||
InvenTree is [available via Docker](https://hub.docker.com/r/inventree/inventree). Read the [docker guide](https://inventree.readthedocs.io/en/latest/start/docker/) for full details.
|
InvenTree is [available via Docker](https://hub.docker.com/r/inventree/inventree). Read the [docker guide](https://inventree.readthedocs.io/en/latest/start/docker/) for full details.
|
||||||
|
|
||||||
# Companion App
|
# Mobile App
|
||||||
|
|
||||||
InvenTree is supported by a [companion mobile app](https://inventree.readthedocs.io/en/latest/app/app/) which allows users access to stock control information and functionality.
|
InvenTree is supported by a [companion mobile app](https://inventree.readthedocs.io/en/latest/app/app/) which allows users access to stock control information and functionality.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
"""
|
||||||
|
On release, ensure that the release tag matches the InvenTree version number!
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
version_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py')
|
||||||
|
|
||||||
|
with open(version_file, 'r') as f:
|
||||||
|
|
||||||
|
results = re.findall(r'INVENTREE_SW_VERSION = "(.*)"', f.read())
|
||||||
|
|
||||||
|
if not len(results) == 1:
|
||||||
|
print(f"Could not find INVENTREE_SW_VERSION in {version_file}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
version = results[0]
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('tag', help='Version tag', action='store')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.tag == version:
|
||||||
|
print(f"Release tag '{args.tag}' does not match INVENTREE_SW_VERSION '{version}'")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
+1
-1
@@ -10,7 +10,7 @@ server {
|
|||||||
proxy_pass http://inventree-server:8000;
|
proxy_pass http://inventree-server:8000;
|
||||||
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $http_host;
|
||||||
|
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user