Merge remote-tracking branch 'inventree/master'

This commit is contained in:
Oliver Walters 2022-07-03 20:48:32 +10:00
commit 6a0da08f3f
72 changed files with 19130 additions and 17588 deletions
.github
.pre-commit-config.yaml
InvenTree
InvenTree
build
common
company
label
locale
cs/LC_MESSAGES
de/LC_MESSAGES
el/LC_MESSAGES
en/LC_MESSAGES
es/LC_MESSAGES
es_MX/LC_MESSAGES
fa/LC_MESSAGES
fr/LC_MESSAGES
he/LC_MESSAGES
hu/LC_MESSAGES
id/LC_MESSAGES
it/LC_MESSAGES
ja/LC_MESSAGES
ko/LC_MESSAGES
nl/LC_MESSAGES
no/LC_MESSAGES
pl/LC_MESSAGES
pt/LC_MESSAGES
pt_br/LC_MESSAGES
ru/LC_MESSAGES
sv/LC_MESSAGES
th/LC_MESSAGES
tr/LC_MESSAGES
vi/LC_MESSAGES
zh/LC_MESSAGES
order
part
plugin
report
stock
templates
docker
requirements-dev.inrequirements-dev.txtrequirements.inrequirements.txttasks.py

@ -15,6 +15,10 @@ inputs:
required: false
description: 'Install the InvenTree requirements?'
default: 'false'
dev-install:
required: false
description: 'Install the InvenTree development requirements?'
default: 'false'
update:
required: false
description: 'Should a full update cycle be run?'
@ -72,6 +76,10 @@ runs:
sudo apt-get install ${{ inputs.apt-dependency }}
# Invoke commands
- name: Install dev requirements
if: ${{ inputs.dev-install == 'true' ||inputs.install == 'true' }}
shell: bash
run: pip install -r requirements-dev.txt
- name: Run invoke install
if: ${{ inputs.install == 'true' }}
shell: bash

@ -39,10 +39,13 @@ jobs:
python3 ci/version_check.py
echo "git_commit_hash=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
echo "git_commit_date=$(git show -s --format=%ci)" >> $GITHUB_ENV
- name: Run Unit Tests
- name: Build Docker Image
run: |
docker-compose build
- name: Run Unit Tests
run: |
docker-compose run inventree-dev-server invoke update
docker-compose run inventree-dev-server invoke setup-dev
docker-compose up -d
docker-compose run inventree-dev-server invoke wait
docker-compose run inventree-dev-server invoke test

@ -34,7 +34,7 @@ jobs:
- name: Enviroment Setup
uses: ./.github/actions/setup
with:
install: true
dev-install: true
- name: Run flake8
run: flake8 InvenTree --extend-ignore=D
@ -119,6 +119,7 @@ jobs:
uses: ./.github/actions/setup
with:
apt-dependency: gettext poppler-utils
dev-install: true
update: true
- name: Download Python Code For `${{ env.wrapper_name }}`
run: git clone --depth 1 https://github.com/inventree/${{ env.wrapper_name }} ./${{ env.wrapper_name }}
@ -168,6 +169,7 @@ jobs:
uses: ./.github/actions/setup
with:
apt-dependency: gettext poppler-utils
dev-install: true
update: true
- name: Coverage Tests
run: invoke coverage
@ -218,6 +220,7 @@ jobs:
with:
apt-dependency: gettext poppler-utils libpq-dev
pip-dependency: psycopg2 django-redis>=5.0.0
dev-install: true
update: true
- name: Run Tests
run: invoke test
@ -261,6 +264,7 @@ jobs:
with:
apt-dependency: gettext poppler-utils libmysqlclient-dev
pip-dependency: mysqlclient
dev-install: true
update: true
- name: Run Tests
run: invoke test

@ -28,3 +28,14 @@ repos:
rev: '5.10.1'
hooks:
- id: isort
- repo: https://github.com/jazzband/pip-tools
rev: 6.6.2
hooks:
- id: pip-compile
name: pip-compile requirements-dev.in
args: [--generate-hashes, requirements-dev.in, -o, requirements-dev.txt]
files: ^requirements-dev\.(in|txt)$
- id: pip-compile
name: pip-compile requirements.txt
args: [requirements.in, -o, requirements.txt]
files: ^requirements\.(in|txt)$

@ -33,6 +33,7 @@ class InvenTreeMoneySerializer(MoneyField):
"""Overrite default values."""
kwargs["max_digits"] = kwargs.get("max_digits", 19)
kwargs["decimal_places"] = kwargs.get("decimal_places", 4)
kwargs["required"] = kwargs.get("required", False)
super().__init__(*args, **kwargs)

@ -253,7 +253,6 @@ INSTALLED_APPS = [
'django_cleanup.apps.CleanupConfig', # Automatically delete orphaned MEDIA files
'mptt', # Modified Preorder Tree Traversal
'markdownify', # Markdown template rendering
'django_admin_shell', # Python shell for the admin interface
'djmoney', # django-money integration
'djmoney.contrib.exchange', # django-money exchange rates
'error_report', # Error reporting in the admin interface
@ -532,7 +531,7 @@ if "postgres" in db_engine: # pragma: no cover
# https://docs.djangoproject.com/en/3.2/ref/databases/#isolation-level
if "isolation_level" not in db_options:
serializable = _is_true(
os.getenv("INVENTREE_DB_ISOLATION_SERIALIZABLE", "true")
os.getenv("INVENTREE_DB_ISOLATION_SERIALIZABLE", "false")
)
db_options["isolation_level"] = (
ISOLATION_LEVEL_SERIALIZABLE
@ -904,11 +903,13 @@ MARKDOWNIFY_BLEACH = False
SENTRY_ENABLED = get_setting('INVENTREE_SENTRY_ENABLED', CONFIG.get('sentry_enabled', False))
SENTRY_DSN = get_setting('INVENTREE_SENTRY_DSN', CONFIG.get('sentry_dsn', INVENTREE_DSN))
SENTRY_SAMPLE_RATE = float(get_setting('INVENTREE_SENTRY_SAMPLE_RATE', CONFIG.get('sentry_sample_rate', 0.1)))
if SENTRY_ENABLED and SENTRY_DSN: # pragma: no cover
sentry_sdk.init(
dsn=SENTRY_DSN,
integrations=[DjangoIntegration(), ],
traces_sample_rate=1.0 if DEBUG else 0.15,
traces_sample_rate=1.0 if DEBUG else SENTRY_SAMPLE_RATE,
send_default_pii=True
)
inventree_tags = {

@ -61,7 +61,7 @@ def raise_warning(msg):
warnings.warn(msg)
def offload_task(taskname, *args, force_sync=False, **kwargs):
def offload_task(taskname, *args, force_async=False, force_sync=False, **kwargs):
"""Create an AsyncTask if workers are running. This is different to a 'scheduled' task, in that it only runs once!
If workers are not running or force_sync flag
@ -79,7 +79,7 @@ def offload_task(taskname, *args, force_sync=False, **kwargs):
except (OperationalError, ProgrammingError): # pragma: no cover
raise_warning(f"Could not offload task '{taskname}' - database not ready")
if is_worker_running() and not force_sync: # pragma: no cover
if force_async or (is_worker_running() and not force_sync):
# Running as asynchronous task
try:
task = AsyncTask(taskname, *args, **kwargs)

@ -9,6 +9,7 @@ from unittest import mock
import django.core.exceptions as django_exceptions
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.sites.models import Site
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
@ -19,9 +20,11 @@ from djmoney.money import Money
import InvenTree.tasks
from common.models import InvenTreeSetting
from common.settings import currency_codes
from stock.models import StockLocation
from part.models import Part, PartCategory
from stock.models import StockItem, StockLocation
from . import config, helpers, ready, status, version
from .tasks import offload_task
from .validators import validate_overage, validate_part_name
@ -604,3 +607,61 @@ class TestInstanceName(helpers.InvenTreeTestCase):
InvenTreeSetting.set_setting("INVENTREE_INSTANCE", "Testing title", self.user)
self.assertEqual(version.inventreeInstanceTitle(), 'Testing title')
# The site should also be changed
site_obj = Site.objects.all().order_by('id').first()
self.assertEqual(site_obj.name, 'Testing title')
def test_instance_url(self):
"""Test instance url settings."""
# Set up required setting
InvenTreeSetting.set_setting("INVENTREE_BASE_URL", "http://127.1.2.3", self.user)
# The site should also be changed
site_obj = Site.objects.all().order_by('id').first()
self.assertEqual(site_obj.domain, 'http://127.1.2.3')
class TestOffloadTask(helpers.InvenTreeTestCase):
"""Tests for offloading tasks to the background worker"""
fixtures = [
'category',
'part',
'location',
'stock',
]
def test_offload_tasks(self):
"""Test that we can offload various tasks to the background worker thread.
This set of tests also ensures that various types of objects
can be encoded by the django-q serialization layer!
Note that as the background worker is not actually running for the tests,
the call to 'offload_task' won't really *do* anything!
However, it serves as a validation that object serialization works!
Ref: https://github.com/inventree/InvenTree/pull/3273
"""
offload_task(
'dummy_tasks.parts',
part=Part.objects.get(pk=1),
cat=PartCategory.objects.get(pk=1),
force_async=True
)
offload_task(
'dummy_tasks.stock',
item=StockItem.objects.get(pk=1),
loc=StockLocation.objects.get(pk=1),
force_async=True
)
offload_task(
'dummy_task.numbers',
1, 2, 3, 4, 5,
force_async=True
)

@ -152,7 +152,6 @@ frontendpatterns = [
# admin sites
re_path(f'^{settings.INVENTREE_ADMIN_URL}/error_log/', include('error_report.urls')),
re_path(f'^{settings.INVENTREE_ADMIN_URL}/shell/', include('django_admin_shell.urls')),
re_path(f'^{settings.INVENTREE_ADMIN_URL}/', admin.site.urls, name='inventree-admin'),
# DB user sessions

@ -420,7 +420,7 @@ class BuildItemList(ListCreateAPI):
DjangoFilterBackend,
]
filter_fields = [
filterset_fields = [
'build',
'stock_item',
'bom_item',
@ -438,7 +438,7 @@ class BuildAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
DjangoFilterBackend,
]
filter_fields = [
filterset_fields = [
'build',
]

@ -1018,6 +1018,20 @@ class Build(MPTTModel, ReferenceIndexingMixin):
"""Returns True if the un-tracked parts are fully allocated for this BuildOrder."""
return self.is_fully_allocated(None)
def has_overallocated_parts(self, output):
"""Check if parts have been 'over-allocated' against the specified output.
Note: If output=None, test un-tracked parts
"""
bom_items = self.tracked_bom_items if output else self.untracked_bom_items
for bom_item in bom_items:
if self.allocated_quantity(bom_item, output) > self.required_quantity(bom_item, output):
return True
return False
def unallocated_bom_items(self, output):
"""Return a list of bom items which have *not* been fully allocated against a particular output."""
unallocated = []

@ -466,6 +466,22 @@ class BuildCancelSerializer(serializers.Serializer):
class BuildCompleteSerializer(serializers.Serializer):
"""DRF serializer for marking a BuildOrder as complete."""
accept_overallocated = serializers.BooleanField(
label=_('Accept Overallocated'),
help_text=_('Accept stock items which have been overallocated to this build order'),
required=False,
default=False,
)
def validate_accept_overallocated(self, value):
"""Check if the 'accept_overallocated' field is required"""
build = self.context['build']
if build.has_overallocated_parts(output=None) and not value:
raise ValidationError(_('Some stock items have been overallocated'))
return value
accept_unallocated = serializers.BooleanField(
label=_('Accept Unallocated'),
help_text=_('Accept that stock items have not been fully allocated to this build order'),

@ -289,7 +289,7 @@ class NotificationList(BulkDeleteMixin, ListAPI):
'message',
]
filter_fields = [
filterset_fields = [
'category',
'read',
]

@ -21,6 +21,7 @@ from django.contrib.auth.models import Group, User
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.humanize.templatetags.humanize import naturaltime
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.core.exceptions import AppRegistryNotReady, ValidationError
from django.core.validators import MinValueValidator, URLValidator
@ -82,6 +83,14 @@ class BaseInvenTreeSetting(models.Model):
super().save()
# Get after_save action
setting = self.get_setting_definition(self.key, *args, **kwargs)
after_save = setting.get('after_save', None)
# Execute if callable
if callable(after_save):
after_save(self)
@property
def cache_key(self):
"""Generate a unique cache key for this settings object"""
@ -735,6 +744,20 @@ def settings_group_options():
return [('', _('No group')), *[(str(a.id), str(a)) for a in Group.objects.all()]]
def update_instance_url(setting):
"""Update the first site objects domain to url."""
site_obj = Site.objects.all().order_by('id').first()
site_obj.domain = setting.value
site_obj.save()
def update_instance_name(setting):
"""Update the first site objects name to instance name."""
site_obj = Site.objects.all().order_by('id').first()
site_obj.name = setting.value
site_obj.save()
class InvenTreeSetting(BaseInvenTreeSetting):
"""An InvenTreeSetting object is a key:value pair used for storing single values (e.g. one-off settings values).
@ -782,6 +805,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'name': _('Server Instance Name'),
'default': 'InvenTree',
'description': _('String descriptor for the server instance'),
'after_save': update_instance_name,
},
'INVENTREE_INSTANCE_TITLE': {
@ -809,6 +833,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'description': _('Base URL for server instance'),
'validator': EmptyURLValidator(),
'default': '',
'after_save': update_instance_url,
},
'INVENTREE_DEFAULT_CURRENCY': {

@ -296,6 +296,13 @@ class InvenTreeNotificationBodies:
)
"""Send when a new order (build, sale or purchase) was created."""
ItemsReceived = NotificationBody(
name=_("Items Received"),
slug='purchase_order.items_received',
message=_('Items have been received against a purchase order'),
template='email/purchase_order_received.html',
)
def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
"""Send out a notification."""

@ -133,6 +133,7 @@ class SettingsTest(InvenTreeTestCase):
'choices',
'units',
'requires_restart',
'after_save',
]
for k in setting.keys():

@ -46,7 +46,7 @@ class CompanyList(ListCreateAPI):
filters.OrderingFilter,
]
filter_fields = [
filterset_fields = [
'is_customer',
'is_manufacturer',
'is_supplier',
@ -169,7 +169,7 @@ class ManufacturerPartAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
DjangoFilterBackend,
]
filter_fields = [
filterset_fields = [
'manufacturer_part',
]
@ -233,7 +233,7 @@ class ManufacturerPartParameterList(ListCreateDestroyAPIView):
filters.OrderingFilter,
]
filter_fields = [
filterset_fields = [
'name',
'value',
'units',
@ -333,7 +333,7 @@ class SupplierPartList(ListCreateDestroyAPIView):
filters.OrderingFilter,
]
filter_fields = [
filterset_fields = [
]
search_fields = [
@ -345,6 +345,7 @@ class SupplierPartList(ListCreateDestroyAPIView):
'part__IPN',
'part__name',
'part__description',
'part__keywords',
]
@ -377,7 +378,7 @@ class SupplierPriceBreakList(ListCreateAPI):
DjangoFilterBackend,
]
filter_fields = [
filterset_fields = [
'part',
]

@ -0,0 +1,20 @@
# Generated by Django 3.2.13 on 2022-06-27 23:08
import company.models
from django.db import migrations
import stdimage.models
class Migration(migrations.Migration):
dependencies = [
('company', '0045_alter_company_notes'),
]
operations = [
migrations.AlterField(
model_name='company',
name='image',
field=stdimage.models.StdImageField(blank=True, force_min_size=False, null=True, upload_to=company.models.rename_company_image, variations={'preview': (256, 256), 'thumbnail': (128, 128)}, verbose_name='Image'),
),
]

@ -14,7 +14,7 @@
<h4>{% trans "Supplier Parts" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% if roles.purchase_order.add %}
{% if roles.purchase_order.add and not part.virtual %}
<button class="btn btn-success" id='supplier-part-create' title='{% trans "Create new supplier part" %}'>
<span class='fas fa-plus-circle'></span> {% trans "New Supplier Part" %}
</button>
@ -61,7 +61,7 @@
<h4>{% trans "Manufacturer Parts" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% if roles.purchase_order.add %}
{% if roles.purchase_order.add and not part.virtual %}
<button type="button" class="btn btn-success" id='manufacturer-part-create' title='{% trans "Create new manufacturer part" %}'>
<span class='fas fa-plus-circle'></span> {% trans "New Manufacturer Part" %}
</button>

@ -31,7 +31,7 @@ class LabelListView(ListAPI):
filters.SearchFilter
]
filter_fields = [
filterset_fields = [
'enabled',
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -70,7 +70,7 @@ class GeneralExtraLineList:
'reference'
]
filter_fields = [
filterset_fields = [
'order',
]
@ -540,7 +540,7 @@ class SalesOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
rest_filters.DjangoFilterBackend,
]
filter_fields = [
filterset_fields = [
'order',
]
@ -672,7 +672,7 @@ class SalesOrderList(APIDownloadMixin, ListCreateAPI):
'reference': ['reference_int', 'reference'],
}
filter_fields = [
filterset_fields = [
'customer',
]
@ -814,7 +814,7 @@ class SalesOrderLineItemList(ListCreateAPI):
'reference',
]
filter_fields = [
filterset_fields = [
'order',
'part',
]
@ -990,7 +990,7 @@ class SalesOrderAllocationList(ListAPI):
]
# Default filterable fields
filter_fields = [
filterset_fields = [
]
@ -1069,7 +1069,7 @@ class PurchaseOrderAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
rest_filters.DjangoFilterBackend,
]
filter_fields = [
filterset_fields = [
'order',
]

@ -0,0 +1,26 @@
# Generated by Django 3.2.13 on 2022-06-28 01:33
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('stock', '0077_alter_stockitem_notes'),
('part', '0079_alter_part_notes'),
('order', '0070_auto_20220620_0728'),
]
operations = [
migrations.AlterField(
model_name='salesorderallocation',
name='item',
field=models.ForeignKey(help_text='Select stock item to allocate', limit_choices_to={'belongs_to': None, 'part__salable': True, 'part__virtual': False, 'sales_order': None}, on_delete=django.db.models.deletion.CASCADE, related_name='sales_order_allocations', to='stock.stockitem', verbose_name='Item'),
),
migrations.AlterField(
model_name='salesorderlineitem',
name='part',
field=models.ForeignKey(help_text='Part', limit_choices_to={'salable': True, 'virtual': False}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales_order_line_items', to='part.part', verbose_name='Part'),
),
]

@ -24,6 +24,7 @@ from mptt.models import TreeForeignKey
import InvenTree.helpers
import InvenTree.ready
from common.notifications import InvenTreeNotificationBodies
from common.settings import currency_code_default
from company.models import Company, SupplierPart
from InvenTree.exceptions import log_error
@ -560,6 +561,14 @@ class PurchaseOrder(Order):
self.received_by = user
self.complete_order() # This will save the model
# Issue a notification to interested parties, that this order has been "updated"
notify_responsible(
self,
PurchaseOrder,
exclude=user,
content=InvenTreeNotificationBodies.ItemsReceived,
)
@receiver(post_save, sender=PurchaseOrder, dispatch_uid='purchase_order_post_save')
def after_save_purchase_order(sender, instance: PurchaseOrder, created: bool, **kwargs):
@ -1094,6 +1103,22 @@ class SalesOrderLineItem(OrderLineItem):
"""Return the API URL associated with the SalesOrderLineItem model"""
return reverse('api-so-line-list')
def clean(self):
"""Perform extra validation steps for this SalesOrderLineItem instance"""
super().clean()
if self.part:
if self.part.virtual:
raise ValidationError({
'part': _("Virtual part cannot be assigned to a sales order")
})
if not self.part.salable:
raise ValidationError({
'part': _("Only salable parts can be assigned to a sales order")
})
order = models.ForeignKey(
SalesOrder,
on_delete=models.CASCADE,
@ -1102,7 +1127,16 @@ class SalesOrderLineItem(OrderLineItem):
help_text=_('Sales Order')
)
part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, verbose_name=_('Part'), help_text=_('Part'), limit_choices_to={'salable': True})
part = models.ForeignKey(
'part.Part', on_delete=models.SET_NULL,
related_name='sales_order_line_items',
null=True,
verbose_name=_('Part'),
help_text=_('Part'),
limit_choices_to={
'salable': True,
'virtual': False,
})
sale_price = InvenTreeModelMoneyField(
max_digits=19,
@ -1400,6 +1434,7 @@ class SalesOrderAllocation(models.Model):
related_name='sales_order_allocations',
limit_choices_to={
'part__salable': True,
'part__virtual': False,
'belongs_to': None,
'sales_order': None,
},

@ -135,7 +135,7 @@ class CategoryList(ListCreateAPI):
filters.OrderingFilter,
]
filter_fields = [
filterset_fields = [
]
ordering_fields = [
@ -281,7 +281,7 @@ class PartSalePriceList(ListCreateAPI):
DjangoFilterBackend
]
filter_fields = [
filterset_fields = [
'part',
]
@ -304,7 +304,7 @@ class PartInternalPriceList(ListCreateAPI):
DjangoFilterBackend
]
filter_fields = [
filterset_fields = [
'part',
]
@ -319,7 +319,7 @@ class PartAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
DjangoFilterBackend,
]
filter_fields = [
filterset_fields = [
'part',
]
@ -866,7 +866,8 @@ class PartFilter(rest_filters.FilterSet):
def filter_in_bom(self, queryset, name, part):
"""Limit queryset to parts in the BOM for the specified part"""
queryset = queryset.filter(id__in=part.get_parts_in_bom())
bom_parts = part.get_parts_in_bom()
queryset = queryset.filter(id__in=[p.pk for p in bom_parts])
return queryset
is_template = rest_filters.BooleanFilter()
@ -1412,7 +1413,7 @@ class PartParameterTemplateList(ListCreateAPI):
filters.SearchFilter,
]
filter_fields = [
filterset_fields = [
'name',
]
@ -1477,7 +1478,7 @@ class PartParameterList(ListCreateAPI):
DjangoFilterBackend
]
filter_fields = [
filterset_fields = [
'part',
'template',
]
@ -1758,7 +1759,7 @@ class BomList(ListCreateDestroyAPIView):
filters.OrderingFilter,
]
filter_fields = [
filterset_fields = [
]
@ -1857,7 +1858,7 @@ class BomItemSubstituteList(ListCreateAPI):
filters.OrderingFilter,
]
filter_fields = [
filterset_fields = [
'part',
'bom_item',
]

@ -0,0 +1,20 @@
# Generated by Django 3.2.13 on 2022-06-27 23:08
from django.db import migrations
import part.models
import stdimage.models
class Migration(migrations.Migration):
dependencies = [
('part', '0079_alter_part_notes'),
]
operations = [
migrations.AlterField(
model_name='part',
name='image',
field=stdimage.models.StdImageField(blank=True, force_min_size=False, null=True, upload_to=part.models.rename_part_image, variations={'preview': (256, 256), 'thumbnail': (128, 128)}, verbose_name='Image'),
),
]

@ -20,7 +20,7 @@
<h4>{% trans "Part Stock" %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% if roles.stock.add %}
{% if roles.stock.add and not part.virtual %}
<button type='button' class='btn btn-success' id='new-stock-item' title='{% trans "Create new stock item" %}'>
<span class='fas fa-plus-circle'></span> {% trans "New Stock Item" %}
</button>

@ -400,6 +400,21 @@ class PartAPITest(InvenTreeAPITestCase):
for part in response.data:
self.assertEqual(part['category'], 2)
def test_filter_by_in_bom(self):
"""Test that we can filter part list by the 'in_bom_for' parameter"""
url = reverse('api-part-list')
response = self.get(
url,
{
'in_bom_for': 100,
},
expected_code=200,
)
self.assertEqual(len(response.data), 4)
def test_filter_by_related(self):
"""Test that we can filter by the 'related' status"""
url = reverse('api-part-list')

@ -1,14 +1,13 @@
"""Utility file to enable simper imports."""
from .helpers import MixinImplementationError, MixinNotImplementedError
from .plugin import IntegrationPluginBase, InvenTreePlugin
from .plugin import InvenTreePlugin
from .registry import registry
__all__ = [
'registry',
'InvenTreePlugin',
'IntegrationPluginBase',
'MixinNotImplementedError',
'MixinImplementationError',
]

@ -62,7 +62,7 @@ class PluginList(ListAPI):
filters.OrderingFilter,
]
filter_fields = [
filterset_fields = [
'active',
]
@ -140,7 +140,7 @@ class PluginSettingList(ListAPI):
DjangoFilterBackend,
]
filter_fields = [
filterset_fields = [
'plugin__active',
'plugin__key',
]

@ -298,16 +298,3 @@ class InvenTreePlugin(MixinBase, MetaBase):
self.package = package
self.sign_state = sign_state
# endregion
class IntegrationPluginBase(InvenTreePlugin):
"""Legacy base class for plugins.
Do not use!
"""
def __init__(self, *args, **kwargs):
"""Send warning about using this reference."""
# TODO remove in 0.8.0
warnings.warn("This import is deprecated - use InvenTreePlugin", DeprecationWarning)
super().__init__(*args, **kwargs)

@ -5,7 +5,7 @@ from datetime import datetime
from django.test import TestCase
import plugin.templatetags.plugin_extras as plugin_tags
from plugin import IntegrationPluginBase, InvenTreePlugin, registry
from plugin import InvenTreePlugin, registry
from plugin.samples.integration.another_sample import (NoIntegrationPlugin,
WrongIntegrationPlugin)
from plugin.samples.integration.sample import SampleIntegrationPlugin
@ -162,11 +162,3 @@ class InvenTreePluginTests(TestCase):
self.assertEqual(self.plugin_old.slug, 'old')
# check default value is used
self.assertEqual(self.plugin_old.get_meta_value('ABC', 'ABCD', '123'), '123')
# check usage of the old class fires
class OldPlugin(IntegrationPluginBase):
pass
with self.assertWarns(DeprecationWarning):
plg = OldPlugin()
self.assertIsInstance(plg, InvenTreePlugin)

@ -34,7 +34,7 @@ class ReportListView(ListAPI):
filters.SearchFilter,
]
filter_fields = [
filterset_fields = [
'enabled',
]

@ -283,7 +283,7 @@ class StockLocationList(ListCreateAPI):
filters.OrderingFilter,
]
filter_fields = [
filterset_fields = [
]
search_fields = [
@ -1064,7 +1064,7 @@ class StockAttachmentList(AttachmentMixin, ListCreateDestroyAPIView):
filters.SearchFilter,
]
filter_fields = [
filterset_fields = [
'stock_item',
]
@ -1095,7 +1095,7 @@ class StockItemTestResultList(ListCreateDestroyAPIView):
filters.OrderingFilter,
]
filter_fields = [
filterset_fields = [
'test',
'user',
'result',
@ -1302,7 +1302,7 @@ class StockTrackingList(ListAPI):
filters.OrderingFilter,
]
filter_fields = [
filterset_fields = [
'item',
'user',
]

@ -342,6 +342,7 @@ class StockItem(MetadataMixin, MPTTModel):
- Unique serial number requirement
- Adds a transaction note when the item is first created.
"""
self.validate_unique()
self.clean()
@ -439,6 +440,7 @@ class StockItem(MetadataMixin, MPTTModel):
The following validation checks are performed:
- The 'part' and 'supplier_part.part' fields cannot point to the same Part object
- The 'part' is not virtual
- The 'part' does not belong to itself
- Quantity must be 1 if the StockItem has a serial number
"""
@ -453,12 +455,18 @@ class StockItem(MetadataMixin, MPTTModel):
self.batch = self.batch.strip()
try:
# Trackable parts must have integer values for quantity field!
if self.part.trackable:
# Trackable parts must have integer values for quantity field!
if self.quantity != int(self.quantity):
raise ValidationError({
'quantity': _('Quantity must be integer value for trackable parts')
})
# Virtual parts cannot have stock items created against them
if self.part.virtual:
raise ValidationError({
'part': _("Stock item cannot be created for virtual parts"),
})
except PartModels.Part.DoesNotExist:
# For some reason the 'clean' process sometimes throws errors because self.part does not exist
# It *seems* that this only occurs in unit testing, though.
@ -582,7 +590,8 @@ class StockItem(MetadataMixin, MPTTModel):
part = models.ForeignKey(
'part.Part', on_delete=models.CASCADE,
verbose_name=_('Base Part'),
related_name='stock_items', help_text=_('Base part'),
related_name='stock_items',
help_text=_('Base part'),
limit_choices_to={
'virtual': False
})

@ -79,6 +79,21 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
- Includes serialization for the item location
"""
part = serializers.PrimaryKeyRelatedField(
queryset=part_models.Part.objects.all(),
many=False, allow_null=False,
help_text=_("Base Part"),
label=_("Part"),
)
def validate_part(self, part):
"""Ensure the provided Part instance is valid"""
if part.virtual:
raise ValidationError(_("Stock item cannot be created for virtual parts"))
return part
def update(self, instance, validated_data):
"""Custom update method to pass the user information through to the instance."""
instance._user = self.context['user']
@ -168,7 +183,11 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
def get_purchase_price_string(self, obj):
"""Return purchase price as string."""
return str(obj.purchase_price) if obj.purchase_price else '-'
if obj.purchase_price:
obj.purchase_price.decimal_places_display = 4
return str(obj.purchase_price)
return '-'
purchase_order_reference = serializers.CharField(source='purchase_order.reference', read_only=True)
sales_order_reference = serializers.CharField(source='sales_order.reference', read_only=True)

@ -0,0 +1,23 @@
{% extends "account/base.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block page_title %}
{% inventree_title %} | {% trans "Permission Denied" %}
{% endblock %}
{% block content %}
<h3>{% trans "Authentication Failure" %}</h3>
<div class='alert alert-danger alert-block'>
{% trans "You have been logged out from InvenTree." %}
</div>
<hr>
<div class='btn-group float-right' role='group'>
<a type='button' class='btn btn-primary' href='{% url "account_login" %}'>
<span class='fas fa-sign-in-alt'></span> {% trans "Login" %}
</a>
</div>
{% endblock %}

@ -0,0 +1,11 @@
{% extends "email/email.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block title %}
{{ message }}
{% if link %}
<p>{% trans "Click on the following link to view this order" %}: <a href='{{ link }}'>{{ link }}</a></p>
{% endif %}
{% endblock title %}

@ -284,6 +284,7 @@ function loadAttachmentTable(url, options) {
},
{
field: 'upload_date',
sortable: true,
title: '{% trans "Upload Date" %}',
formatter: function(value, row) {
var html = renderDate(value);

@ -174,6 +174,7 @@ function completeBuildOrder(build_id, options={}) {
var fields = {
accept_unallocated: {},
accept_overallocated: {},
accept_incomplete: {},
};

@ -429,19 +429,27 @@ function getAvailableTableFilters(tableKey) {
title: '{% trans "Include subcategories" %}',
description: '{% trans "Include parts in subcategories" %}',
},
has_ipn: {
type: 'bool',
title: '{% trans "Has IPN" %}',
description: '{% trans "Part has internal part number" %}',
},
active: {
type: 'bool',
title: '{% trans "Active" %}',
description: '{% trans "Show active parts" %}',
},
is_template: {
assembly: {
type: 'bool',
title: '{% trans "Template" %}',
title: '{% trans "Assembly" %}',
},
unallocated_stock: {
type: 'bool',
title: '{% trans "Available stock" %}',
},
component: {
type: 'bool',
title: '{% trans "Component" %}',
},
has_ipn: {
type: 'bool',
title: '{% trans "Has IPN" %}',
description: '{% trans "Part has internal part number" %}',
},
has_stock: {
type: 'bool',
@ -451,34 +459,30 @@ function getAvailableTableFilters(tableKey) {
type: 'bool',
title: '{% trans "Low stock" %}',
},
unallocated_stock: {
purchaseable: {
type: 'bool',
title: '{% trans "Available stock" %}',
},
assembly: {
type: 'bool',
title: '{% trans "Assembly" %}',
},
component: {
type: 'bool',
title: '{% trans "Component" %}',
},
starred: {
type: 'bool',
title: '{% trans "Subscribed" %}',
title: '{% trans "Purchasable" %}',
},
salable: {
type: 'bool',
title: '{% trans "Salable" %}',
},
starred: {
type: 'bool',
title: '{% trans "Subscribed" %}',
},
is_template: {
type: 'bool',
title: '{% trans "Template" %}',
},
trackable: {
type: 'bool',
title: '{% trans "Trackable" %}',
},
purchaseable: {
virtual: {
type: 'bool',
title: '{% trans "Purchasable" %}',
},
title: '{% trans "Virtual" %}',
}
};
}

@ -374,6 +374,11 @@ $.fn.inventreeTable = function(options) {
options.pageList = [25, 50, 100, 250, 'all'];
options.totalField = 'count';
options.dataField = 'results';
if (options.sidePagination == null) {
options.sidePagination = 'server';
}
} else {
options.pagination = false;
}

@ -9,7 +9,7 @@ invoke>=1.4.0 # Invoke build tool
psycopg2>=2.9.1
mysqlclient>=2.0.3
pgcli>=3.1.0
mariadb>=1.0.7
mariadb>=1.0.7,<1.1.0
# gunicorn web server
gunicorn>=20.1.0

13
requirements-dev.in Normal file

@ -0,0 +1,13 @@
# Dev requirements for InvenTree
-c requirements.txt
coverage # Unit test coverage
coveralls==2.1.2 # Coveralls linking (for tracking coverage) # PINNED 2022-06-28 - Old version needed for correct upload
django-debug-toolbar # Debug / profiling toolbar
django-test-migrations # Unit testing for database migrations
flake8 # PEP checking
flake8-docstrings # docstring format testing
isort # python import sorting
pep8-naming # PEP naming convention extension
pip-tools # Compile pip requirements
pre-commit # Git pre-commit
setuptools # Standard depenedency

279
requirements-dev.txt Normal file

@ -0,0 +1,279 @@
#
# This file is autogenerated by pip-compile with python 3.9
# To update, run:
#
# pip-compile --generate-hashes --output-file=requirements-dev.txt requirements-dev.in
#
asgiref==3.5.2 \
--hash=sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4 \
--hash=sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424
# via
# -c requirements.txt
# django
certifi==2022.6.15 \
--hash=sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d \
--hash=sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412
# via
# -c requirements.txt
# requests
cfgv==3.3.1 \
--hash=sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426 \
--hash=sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736
# via pre-commit
charset-normalizer==2.0.12 \
--hash=sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597 \
--hash=sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df
# via
# -c requirements.txt
# requests
click==8.1.3 \
--hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \
--hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
# via pip-tools
coverage==5.5 \
--hash=sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c \
--hash=sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6 \
--hash=sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45 \
--hash=sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a \
--hash=sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03 \
--hash=sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529 \
--hash=sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a \
--hash=sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a \
--hash=sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2 \
--hash=sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6 \
--hash=sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759 \
--hash=sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53 \
--hash=sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a \
--hash=sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4 \
--hash=sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff \
--hash=sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502 \
--hash=sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793 \
--hash=sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb \
--hash=sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905 \
--hash=sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821 \
--hash=sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b \
--hash=sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81 \
--hash=sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0 \
--hash=sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b \
--hash=sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3 \
--hash=sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184 \
--hash=sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701 \
--hash=sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a \
--hash=sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82 \
--hash=sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638 \
--hash=sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5 \
--hash=sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083 \
--hash=sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6 \
--hash=sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90 \
--hash=sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465 \
--hash=sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a \
--hash=sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3 \
--hash=sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e \
--hash=sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066 \
--hash=sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf \
--hash=sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b \
--hash=sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae \
--hash=sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669 \
--hash=sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873 \
--hash=sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b \
--hash=sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6 \
--hash=sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb \
--hash=sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160 \
--hash=sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c \
--hash=sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079 \
--hash=sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d \
--hash=sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6
# via
# -r requirements-dev.in
# coveralls
coveralls==2.1.2 \
--hash=sha256:4430b862baabb3cf090d36d84d331966615e4288d8a8c5957e0fd456d0dd8bd6 \
--hash=sha256:b3b60c17b03a0dee61952a91aed6f131e0b2ac8bd5da909389c53137811409e1
# via -r requirements-dev.in
distlib==0.3.4 \
--hash=sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b \
--hash=sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579
# via virtualenv
django==3.2.13 \
--hash=sha256:6d93497a0a9bf6ba0e0b1a29cccdc40efbfc76297255b1309b3a884a688ec4b6 \
--hash=sha256:b896ca61edc079eb6bbaa15cf6071eb69d6aac08cce5211583cfb41515644fdf
# via
# -c requirements.txt
# django-debug-toolbar
django-debug-toolbar==3.5.0 \
--hash=sha256:89a52128309eb4da12738801ff0c202d2ff8730d1c3225fac6acf630c303e661 \
--hash=sha256:97965f2630692de316ea0c1ca5bfa81660d7ba13146dbc6be2059cf55b35d0e5
# via -r requirements-dev.in
django-test-migrations==1.2.0 \
--hash=sha256:874884ff4e980583cd9f1c986bb9fbfe72b436e759be23004e4f52c26a15f363 \
--hash=sha256:9e8b9b4364fef70dde10a5f85c5a75d447ca2189ec648325610fab1268daec97
# via -r requirements-dev.in
docopt==0.6.2 \
--hash=sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491
# via coveralls
filelock==3.7.1 \
--hash=sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404 \
--hash=sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04
# via virtualenv
flake8==4.0.1 \
--hash=sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d \
--hash=sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d
# via
# -r requirements-dev.in
# flake8-docstrings
# pep8-naming
flake8-docstrings==1.6.0 \
--hash=sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde \
--hash=sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b
# via -r requirements-dev.in
identify==2.5.1 \
--hash=sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa \
--hash=sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82
# via pre-commit
idna==3.3 \
--hash=sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff \
--hash=sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d
# via
# -c requirements.txt
# requests
isort==5.10.1 \
--hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
--hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
# via -r requirements-dev.in
mccabe==0.6.1 \
--hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \
--hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f
# via flake8
nodeenv==1.7.0 \
--hash=sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e \
--hash=sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b
# via pre-commit
pep517==0.12.0 \
--hash=sha256:931378d93d11b298cf511dd634cf5ea4cb249a28ef84160b3247ee9afb4e8ab0 \
--hash=sha256:dd884c326898e2c6e11f9e0b64940606a93eb10ea022a2e067959f3a110cf161
# via pip-tools
pep8-naming==0.13.0 \
--hash=sha256:069ea20e97f073b3e6d4f789af2a57816f281ca64b86210c7d471117a4b6bfd0 \
--hash=sha256:9f38e6dcf867a1fb7ad47f5ff72c0ddae544a6cf64eb9f7600b7b3c0bb5980b5
# via -r requirements-dev.in
pip-tools==6.6.2 \
--hash=sha256:6b486548e5a139e30e4c4a225b3b7c2d46942a9f6d1a91143c21b1de4d02fd9b \
--hash=sha256:f638503a9f77d98d9a7d72584b1508d3f82ed019b8fab24f4e5ad078c1b8c95e
# via -r requirements-dev.in
platformdirs==2.5.2 \
--hash=sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788 \
--hash=sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19
# via virtualenv
pre-commit==2.19.0 \
--hash=sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10 \
--hash=sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615
# via -r requirements-dev.in
pycodestyle==2.8.0 \
--hash=sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20 \
--hash=sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f
# via flake8
pydocstyle==6.1.1 \
--hash=sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc \
--hash=sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4
# via flake8-docstrings
pyflakes==2.4.0 \
--hash=sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c \
--hash=sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e
# via flake8
pytz==2022.1 \
--hash=sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7 \
--hash=sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c
# via
# -c requirements.txt
# django
pyyaml==6.0 \
--hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \
--hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \
--hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \
--hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \
--hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \
--hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \
--hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \
--hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \
--hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \
--hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \
--hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \
--hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \
--hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \
--hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \
--hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \
--hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \
--hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \
--hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \
--hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \
--hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \
--hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \
--hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \
--hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \
--hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \
--hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \
--hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \
--hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \
--hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \
--hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \
--hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \
--hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \
--hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \
--hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5
# via
# -c requirements.txt
# pre-commit
requests==2.28.0 \
--hash=sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f \
--hash=sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b
# via
# -c requirements.txt
# coveralls
six==1.16.0 \
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
# via
# -c requirements.txt
# virtualenv
snowballstemmer==2.2.0 \
--hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
--hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
# via pydocstyle
sqlparse==0.4.2 \
--hash=sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae \
--hash=sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d
# via
# -c requirements.txt
# django
# django-debug-toolbar
toml==0.10.2 \
--hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
--hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
# via pre-commit
tomli==2.0.1 \
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
# via pep517
typing-extensions==4.2.0 \
--hash=sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708 \
--hash=sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376
# via django-test-migrations
urllib3==1.26.9 \
--hash=sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14 \
--hash=sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e
# via
# -c requirements.txt
# requests
virtualenv==20.15.0 \
--hash=sha256:4c44b1d77ca81f8368e2d7414f9b20c428ad16b343ac6d226206c5b84e2b4fcc \
--hash=sha256:804cce4de5b8a322f099897e308eecc8f6e2951f1a8e7e2b3598dff865f01336
# via pre-commit
wheel==0.37.1 \
--hash=sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a \
--hash=sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4
# via pip-tools
# WARNING: The following packages were not pinned, but pip requires them to be
# pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag.
# pip
# setuptools

37
requirements.in Normal file

@ -0,0 +1,37 @@
# Please keep this list sorted - if you pin a version provide a reason
Django<4 # Django package
coreapi # API documentation for djangorestframework
django-allauth # SSO for external providers via OpenID
django-allauth-2fa # MFA / 2FA
django-cleanup # Automated deletion of old / unused uploaded files
django-cors-headers # CORS headers extension for DRF
django-crispy-forms # Form helpers
django-error-report # Error report viewer for the admin interface
django-filter # Extended filtering options
django-formtools # Form wizard tools
django-import-export==2.5.0 # Data import / export for admin interface
django-maintenance-mode # Shut down application while reloading etc.
django-markdownify # Markdown rendering
django-money<3.0.0 # Django app for currency management # FIXED 2022-06-26 to make sure py-moneyed is not conflicting
django-mptt==0.11.0 # Modified Preorder Tree Traversal
django-redis>=5.0.0 # Redis integration
django-q # Background task scheduling
django-sql-utils # Advanced query annotation / aggregation
django-sslserver # Secure HTTP development server
django-stdimage<6.0.0 # Advanced ImageField management # FIXED 2022-06-29 6.0.0 breaks serialization for django-q
django-user-sessions # user sessions in DB
django-weasyprint # django weasyprint integration
djangorestframework # DRF framework
django-xforwardedfor-middleware # IP forwarding metadata
gunicorn # Gunicorn web server
pdf2image # PDF to image conversion
pillow # Image manipulation
python-barcode[images] # Barcode generator
qrcode[pil] # QR code generator
rapidfuzz==0.7.6 # Fuzzy string matching
sentry-sdk # Error reporting (optional)
setuptools # Standard depenedency
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
# Fixed sub-dependencies
py-moneyed<2.0 # For django-money # FIXED 2022-06-18 as we need `moneyed.localization`

@ -1,51 +1,249 @@
# Please keep this list sorted
Django==3.2.13 # Django package
bleach==4.1.0 # HTML santization
certifi # Certifi is (most likely) installed through one of the requirements above
coreapi==2.3.0 # API documentation
coverage==5.3 # Unit test coverage
coveralls==2.1.2 # Coveralls linking (for Travis)
cryptography==3.4.8 # Cryptography support
django-admin-shell==0.1.2 # Python shell for the admin interface
django-allauth==0.48.0 # SSO for external providers via OpenID
django-allauth-2fa==0.9 # MFA / 2FA # IMPORTANT: Do only change after reviewing GHSA-8j76-mm54-52xq
django-cleanup==5.1.0 # Manage deletion of old / unused uploaded files
django-cors-headers==3.2.0 # CORS headers extension for DRF
django-crispy-forms==1.11.2 # Form helpers
django-debug-toolbar==3.2.4 # Debug / profiling toolbar
django-error-report==0.2.0 # Error report viewer for the admin interface
django-filter==2.4.0 # Extended filtering options
django-formtools==2.3 # Form wizard tools
django-import-export==2.5.0 # Data import / export for admin interface
django-maintenance-mode==0.16.1 # Shut down application while reloading etc.
django-markdownify==0.8.0 # Markdown rendering
django-money==1.1 # Django app for currency management
django-mptt==0.11.0 # Modified Preorder Tree Traversal
django-redis>=5.0.0 # Redis integration
django-q==1.3.4 # Background task scheduling
django-sql-utils==0.5.0 # Advanced query annotation / aggregation
django-sslserver==0.22 # Secure HTTP development server
django-stdimage==5.1.1 # Advanced ImageField management
django-test-migrations==1.1.0 # Unit testing for database migrations
django-user-sessions==1.7.1 # user sessions in DB
django-weasyprint==2.1.0 # django weasyprint integration
djangorestframework==3.12.4 # DRF framework
django-xforwardedfor-middleware==2.0 # IP forwarding metadata
flake8==3.8.3 # PEP checking
flake8-docstrings==1.6.0 # docstring format testing
gunicorn>=20.1.0 # Gunicorn web server
importlib_metadata # Backport for importlib.metadata
isort==5.10.1 # DEV: python import sorting
markdown==3.3.4 # Force particular version of markdown
pdf2image==1.16.0 # PDF to image conversion
pep8-naming==0.11.1 # PEP naming convention extension
pre-commit==2.19.0 # Git pre-commit
pillow==9.1.1 # Image manipulation
py-moneyed==0.8.0 # Specific version requirement for py-moneyed
pygments==2.7.4 # Syntax highlighting
python-barcode[images]==0.13.1 # Barcode generator
qrcode[pil]==6.1 # QR code generator
rapidfuzz==0.7.6 # Fuzzy string matching
sentry-sdk==1.5.12 # Error reporting (optional)
tablib[xls,xlsx,yaml] # Support for XLS and XLSX formats
weasyprint==55.0 # PDF generation library
#
# This file is autogenerated by pip-compile with python 3.9
# To update, run:
#
# pip-compile --output-file=requirements.txt requirements.in
#
arrow==1.2.2
# via django-q
asgiref==3.5.2
# via django
babel==2.10.3
# via py-moneyed
bleach[css]==5.0.0
# via django-markdownify
blessed==1.19.1
# via django-q
brotli==1.0.9
# via fonttools
certifi==2022.6.15
# via
# requests
# sentry-sdk
cffi==1.15.0
# via
# cryptography
# weasyprint
charset-normalizer==2.0.12
# via requests
coreapi==2.3.3
# via -r requirements.in
coreschema==0.0.4
# via coreapi
cryptography==37.0.2
# via pyjwt
cssselect2==0.6.0
# via weasyprint
defusedxml==0.7.1
# via
# odfpy
# python3-openid
diff-match-patch==20200713
# via django-import-export
django==3.2.13
# via
# -r requirements.in
# django-allauth
# django-allauth-2fa
# django-cors-headers
# django-error-report
# django-filter
# django-formtools
# django-import-export
# django-js-asset
# django-markdownify
# django-money
# django-mptt
# django-otp
# django-picklefield
# django-q
# django-redis
# django-sql-utils
# django-sslserver
# django-stdimage
# django-user-sessions
# django-weasyprint
# django-xforwardedfor-middleware
# djangorestframework
django-allauth==0.51.0
# via
# -r requirements.in
# django-allauth-2fa
django-allauth-2fa==0.9
# via -r requirements.in
django-cleanup==6.0.0
# via -r requirements.in
django-cors-headers==3.13.0
# via -r requirements.in
django-crispy-forms==1.14.0
# via -r requirements.in
django-error-report==0.2.0
# via -r requirements.in
django-filter==22.1
# via -r requirements.in
django-formtools==2.3
# via -r requirements.in
django-import-export==2.5.0
# via -r requirements.in
django-js-asset==2.0.0
# via django-mptt
django-maintenance-mode==0.16.3
# via -r requirements.in
django-markdownify==0.9.2
# via -r requirements.in
django-money==2.1.1
# via -r requirements.in
django-mptt==0.11.0
# via -r requirements.in
django-otp==1.1.3
# via django-allauth-2fa
django-picklefield==3.1
# via django-q
django-q==1.3.9
# via -r requirements.in
django-redis==5.2.0
# via -r requirements.in
django-sql-utils==0.6.1
# via -r requirements.in
django-sslserver==0.22
# via -r requirements.in
django-stdimage==5.3.0
# via -r requirements.in
django-user-sessions==1.7.1
# via -r requirements.in
django-weasyprint==2.1.0
# via -r requirements.in
django-xforwardedfor-middleware==2.0
# via -r requirements.in
djangorestframework==3.13.1
# via -r requirements.in
et-xmlfile==1.1.0
# via openpyxl
fonttools[woff]==4.33.3
# via weasyprint
gunicorn==20.1.0
# via -r requirements.in
html5lib==1.1
# via weasyprint
idna==3.3
# via requests
importlib-metadata==4.12.0
# via markdown
itypes==1.2.0
# via coreapi
jinja2==3.1.2
# via coreschema
markdown==3.3.7
# via django-markdownify
markuppy==1.14
# via tablib
markupsafe==2.1.1
# via jinja2
oauthlib==3.2.0
# via requests-oauthlib
odfpy==1.4.1
# via tablib
openpyxl==3.0.10
# via tablib
pdf2image==1.16.0
# via -r requirements.in
pillow==9.1.1
# via
# -r requirements.in
# django-stdimage
# pdf2image
# python-barcode
# qrcode
# weasyprint
py-moneyed==1.2
# via
# -r requirements.in
# django-money
pycparser==2.21
# via cffi
pydyf==0.2.0
# via weasyprint
pyjwt[crypto]==2.4.0
# via django-allauth
pyphen==0.12.0
# via weasyprint
python-barcode[images]==0.14.0
# via -r requirements.in
python-dateutil==2.8.2
# via arrow
python-fsutil==0.6.1
# via django-maintenance-mode
python3-openid==3.2.0
# via django-allauth
pytz==2022.1
# via
# babel
# django
# djangorestframework
pyyaml==6.0
# via tablib
qrcode[pil]==7.3.1
# via
# -r requirements.in
# django-allauth-2fa
rapidfuzz==0.7.6
# via -r requirements.in
redis==3.5.3
# via
# django-q
# django-redis
requests==2.28.0
# via
# coreapi
# django-allauth
# requests-oauthlib
requests-oauthlib==1.3.1
# via django-allauth
sentry-sdk==1.6.0
# via -r requirements.in
six==1.16.0
# via
# bleach
# blessed
# html5lib
# python-dateutil
sqlparse==0.4.2
# via
# django
# django-sql-utils
tablib[html,ods,xls,xlsx,yaml]==3.2.1
# via
# -r requirements.in
# django-import-export
tinycss2==1.1.1
# via
# bleach
# cssselect2
# weasyprint
uritemplate==4.1.1
# via coreapi
urllib3==1.26.9
# via
# requests
# sentry-sdk
wcwidth==0.2.5
# via blessed
weasyprint==55.0
# via django-weasyprint
webencodings==0.5.1
# via
# bleach
# cssselect2
# html5lib
# tinycss2
xlrd==2.0.1
# via tablib
xlwt==1.3.0
# via tablib
zipp==3.8.0
# via importlib-metadata
zopfli==0.2.1
# via fonttools
# The following packages are considered to be unsafe in a requirements file:
# setuptools

@ -111,10 +111,10 @@ def install(c):
@task
def setup_dev(c):
"""Sets up everything needed for the dev enviroment."""
print("Installing required python packages from 'requirements.txt'")
print("Installing required python packages from 'requirements-dev.txt'")
# Install required Python packages with PIP
c.run('pip3 install -U -r requirements.txt')
c.run('pip3 install -U -r requirements-dev.txt')
# Install pre-commit hook
c.run('pre-commit install')