mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-02 05:26:45 +00:00
Add ProjectCode support to build orders (#4808)
* Add "project_code" field to Build model * Add "project_code" field to Build model * build javascript updates (cherry picked from commit 3e27a3b739c0925aeb1e09fd027d4a6183bad4ef) * Update table filters (cherry picked from commit 196c67558591e52c4fc84db54b7ed8468e952116) * Adds API filtering * Bump API version * Hide project code field from build form if project codes not enabled (cherry picked from commit 4e210e3dfa72f87f3a0a8ca51d25f076d9533b53) * refactoring to attempt to fix circular imports * Upgrade django-test-migrations package * Fix broken import * Further fixes for unit tests * Update unit tests for migration files * Fix typo in build.js * Migration test updates - Need to specify MPTT stuff * Fix build.js * Fix migration order * Update API version
This commit is contained in:
parent
c8365ccd0c
commit
00bb740216
@ -2,17 +2,23 @@
|
|||||||
|
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 120
|
INVENTREE_API_VERSION = 121
|
||||||
|
|
||||||
"""
|
"""
|
||||||
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
|
||||||
|
|
||||||
|
v121 -> 2023-06-14 : https://github.com/inventree/InvenTree/pull/4808
|
||||||
|
- Adds "ProjectCode" link to Build model
|
||||||
|
|
||||||
v120 -> 2023-06-07 : https://github.com/inventree/InvenTree/pull/4855
|
v120 -> 2023-06-07 : https://github.com/inventree/InvenTree/pull/4855
|
||||||
- Major overhaul of the build order API
|
- Major overhaul of the build order API
|
||||||
- Adds new BuildLine model
|
- Adds new BuildLine model
|
||||||
|
|
||||||
|
v120 -> 2023-06-12 : https://github.com/inventree/InvenTree/pull/4804
|
||||||
|
- Adds 'project_code' field to build order API endpoints
|
||||||
|
|
||||||
v119 -> 2023-06-01 : https://github.com/inventree/InvenTree/pull/4898
|
v119 -> 2023-06-01 : https://github.com/inventree/InvenTree/pull/4898
|
||||||
- Add Metadata to: Part test templates, Part parameters, Part category parameter templates, BOM item substitute, Part relateds, Stock item test result
|
- Add Metadata to: Part test templates, Part parameters, Part category parameter templates, BOM item substitute, Related Parts, Stock item test result
|
||||||
|
|
||||||
v118 -> 2023-06-01 : https://github.com/inventree/InvenTree/pull/4935
|
v118 -> 2023-06-01 : https://github.com/inventree/InvenTree/pull/4935
|
||||||
- Adds extra fields for the PartParameterTemplate model
|
- Adds extra fields for the PartParameterTemplate model
|
||||||
@ -30,6 +36,7 @@ v115 - > 2023-05-18 : https://github.com/inventree/InvenTree/pull/4846
|
|||||||
|
|
||||||
v114 -> 2023-05-16 : https://github.com/inventree/InvenTree/pull/4825
|
v114 -> 2023-05-16 : https://github.com/inventree/InvenTree/pull/4825
|
||||||
- Adds "delivery_date" to shipments
|
- Adds "delivery_date" to shipments
|
||||||
|
>>>>>>> inventree/master
|
||||||
|
|
||||||
v113 -> 2023-05-13 : https://github.com/inventree/InvenTree/pull/4800
|
v113 -> 2023-05-13 : https://github.com/inventree/InvenTree/pull/4800
|
||||||
- Adds API endpoints for scrapping a build output
|
- Adds API endpoints for scrapping a build output
|
||||||
|
@ -14,7 +14,6 @@ from allauth_2fa.middleware import (AllauthTwoFactorMiddleware,
|
|||||||
from error_report.middleware import ExceptionProcessor
|
from error_report.middleware import ExceptionProcessor
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
|
||||||
from InvenTree.urls import frontendpatterns
|
from InvenTree.urls import frontendpatterns
|
||||||
|
|
||||||
logger = logging.getLogger("inventree")
|
logger = logging.getLogger("inventree")
|
||||||
@ -123,6 +122,9 @@ class Check2FAMiddleware(BaseRequire2FAMiddleware):
|
|||||||
"""Check if user is required to have MFA enabled."""
|
"""Check if user is required to have MFA enabled."""
|
||||||
def require_2fa(self, request):
|
def require_2fa(self, request):
|
||||||
"""Use setting to check if MFA should be enforced for frontend page."""
|
"""Use setting to check if MFA should be enforced for frontend page."""
|
||||||
|
|
||||||
|
from common.models import InvenTreeSetting
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if url_matcher.resolve(request.path[1:]):
|
if url_matcher.resolve(request.path[1:]):
|
||||||
return InvenTreeSetting.get_setting('LOGIN_ENFORCE_MFA')
|
return InvenTreeSetting.get_setting('LOGIN_ENFORCE_MFA')
|
||||||
|
@ -21,7 +21,7 @@ from rest_framework.serializers import DecimalField
|
|||||||
from rest_framework.utils import model_meta
|
from rest_framework.utils import model_meta
|
||||||
from taggit.serializers import TaggitSerializer
|
from taggit.serializers import TaggitSerializer
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
import common.models as common_models
|
||||||
from common.settings import currency_code_default, currency_code_mappings
|
from common.settings import currency_code_default, currency_code_mappings
|
||||||
from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField
|
from InvenTree.fields import InvenTreeRestURLField, InvenTreeURLField
|
||||||
from InvenTree.helpers_model import download_image_from_url
|
from InvenTree.helpers_model import download_image_from_url
|
||||||
@ -724,7 +724,7 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
|
|||||||
if not url:
|
if not url:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_FROM_URL'):
|
if not common_models.InvenTreeSetting.get_setting('INVENTREE_DOWNLOAD_FROM_URL'):
|
||||||
raise ValidationError(_("Downloading images from remote URL is not enabled"))
|
raise ValidationError(_("Downloading images from remote URL is not enabled"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -233,6 +233,11 @@ class ExchangeRateMixin:
|
|||||||
Rate.objects.bulk_create(items)
|
Rate.objects.bulk_create(items)
|
||||||
|
|
||||||
|
|
||||||
|
class InvenTreeTestCase(ExchangeRateMixin, UserMixin, TestCase):
|
||||||
|
"""Testcase with user setup buildin."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
||||||
"""Base class for running InvenTree API tests."""
|
"""Base class for running InvenTree API tests."""
|
||||||
|
|
||||||
@ -408,8 +413,3 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
|
|||||||
data.append(entry)
|
data.append(entry)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class InvenTreeTestCase(ExchangeRateMixin, UserMixin, TestCase):
|
|
||||||
"""Testcase with user setup buildin."""
|
|
||||||
pass
|
|
||||||
|
@ -31,8 +31,8 @@ from allauth_2fa.views import TwoFactorRemove
|
|||||||
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
||||||
from user_sessions.views import SessionDeleteOtherView, SessionDeleteView
|
from user_sessions.views import SessionDeleteOtherView, SessionDeleteView
|
||||||
|
|
||||||
from common.models import ColorTheme, InvenTreeSetting
|
import common.models as common_models
|
||||||
from common.settings import currency_code_default, currency_codes
|
import common.settings as common_settings
|
||||||
from part.models import PartCategory
|
from part.models import PartCategory
|
||||||
from users.models import RuleSet, check_user_role
|
from users.models import RuleSet, check_user_role
|
||||||
|
|
||||||
@ -514,10 +514,10 @@ class SettingsView(TemplateView):
|
|||||||
"""Add data for template."""
|
"""Add data for template."""
|
||||||
ctx = super().get_context_data(**kwargs).copy()
|
ctx = super().get_context_data(**kwargs).copy()
|
||||||
|
|
||||||
ctx['settings'] = InvenTreeSetting.objects.all().order_by('key')
|
ctx['settings'] = common_models.InvenTreeSetting.objects.all().order_by('key')
|
||||||
|
|
||||||
ctx["base_currency"] = currency_code_default()
|
ctx["base_currency"] = common_settings.currency_code_default()
|
||||||
ctx["currencies"] = currency_codes
|
ctx["currencies"] = common_settings.currency_codes
|
||||||
|
|
||||||
ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange")
|
ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange")
|
||||||
|
|
||||||
@ -622,8 +622,8 @@ class AppearanceSelectView(RedirectView):
|
|||||||
def get_user_theme(self):
|
def get_user_theme(self):
|
||||||
"""Get current user color theme."""
|
"""Get current user color theme."""
|
||||||
try:
|
try:
|
||||||
user_theme = ColorTheme.objects.filter(user=self.request.user).get()
|
user_theme = common_models.ColorTheme.objects.filter(user=self.request.user).get()
|
||||||
except ColorTheme.DoesNotExist:
|
except common_models.ColorTheme.DoesNotExist:
|
||||||
user_theme = None
|
user_theme = None
|
||||||
|
|
||||||
return user_theme
|
return user_theme
|
||||||
@ -637,7 +637,7 @@ class AppearanceSelectView(RedirectView):
|
|||||||
|
|
||||||
# Create theme entry if user did not select one yet
|
# Create theme entry if user did not select one yet
|
||||||
if not user_theme:
|
if not user_theme:
|
||||||
user_theme = ColorTheme()
|
user_theme = common_models.ColorTheme()
|
||||||
user_theme.user = request.user
|
user_theme.user = request.user
|
||||||
|
|
||||||
user_theme.name = theme
|
user_theme.name = theme
|
||||||
|
@ -16,6 +16,7 @@ from InvenTree.helpers import str2bool, isNull, DownloadFile
|
|||||||
from InvenTree.status_codes import BuildStatus, BuildStatusGroups
|
from InvenTree.status_codes import BuildStatus, BuildStatusGroups
|
||||||
from InvenTree.mixins import CreateAPI, RetrieveUpdateDestroyAPI, ListCreateAPI
|
from InvenTree.mixins import CreateAPI, RetrieveUpdateDestroyAPI, ListCreateAPI
|
||||||
|
|
||||||
|
import common.models
|
||||||
import build.admin
|
import build.admin
|
||||||
import build.serializers
|
import build.serializers
|
||||||
from build.models import Build, BuildLine, BuildItem, BuildOrderAttachment
|
from build.models import Build, BuildLine, BuildItem, BuildOrderAttachment
|
||||||
@ -89,6 +90,21 @@ class BuildFilter(rest_filters.FilterSet):
|
|||||||
lookup_expr="iexact"
|
lookup_expr="iexact"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
project_code = rest_filters.ModelChoiceFilter(
|
||||||
|
queryset=common.models.ProjectCode.objects.all(),
|
||||||
|
field_name='project_code'
|
||||||
|
)
|
||||||
|
|
||||||
|
has_project_code = rest_filters.BooleanFilter(label='has_project_code', method='filter_has_project_code')
|
||||||
|
|
||||||
|
def filter_has_project_code(self, queryset, name, value):
|
||||||
|
"""Filter by whether or not the order has a project code"""
|
||||||
|
|
||||||
|
if str2bool(value):
|
||||||
|
return queryset.exclude(project_code=None)
|
||||||
|
else:
|
||||||
|
return queryset.filter(project_code=None)
|
||||||
|
|
||||||
|
|
||||||
class BuildList(APIDownloadMixin, ListCreateAPI):
|
class BuildList(APIDownloadMixin, ListCreateAPI):
|
||||||
"""API endpoint for accessing a list of Build objects.
|
"""API endpoint for accessing a list of Build objects.
|
||||||
@ -114,11 +130,13 @@ class BuildList(APIDownloadMixin, ListCreateAPI):
|
|||||||
'completed',
|
'completed',
|
||||||
'issued_by',
|
'issued_by',
|
||||||
'responsible',
|
'responsible',
|
||||||
|
'project_code',
|
||||||
'priority',
|
'priority',
|
||||||
]
|
]
|
||||||
|
|
||||||
ordering_field_aliases = {
|
ordering_field_aliases = {
|
||||||
'reference': ['reference_int', 'reference'],
|
'reference': ['reference_int', 'reference'],
|
||||||
|
'project_code': ['project_code__code'],
|
||||||
}
|
}
|
||||||
|
|
||||||
ordering = '-reference'
|
ordering = '-reference'
|
||||||
@ -129,6 +147,7 @@ class BuildList(APIDownloadMixin, ListCreateAPI):
|
|||||||
'part__name',
|
'part__name',
|
||||||
'part__IPN',
|
'part__IPN',
|
||||||
'part__description',
|
'part__description',
|
||||||
|
'project_code__code',
|
||||||
'priority',
|
'priority',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
20
InvenTree/build/migrations/0048_build_project_code.py
Normal file
20
InvenTree/build/migrations/0048_build_project_code.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 3.2.19 on 2023-05-14 09:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('common', '0019_projectcode_metadata'),
|
||||||
|
('build', '0047_auto_20230606_1058'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='build',
|
||||||
|
name='project_code',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Project code for this build order', null=True, on_delete=django.db.models.deletion.SET_NULL, to='common.projectcode', verbose_name='Project Code'),
|
||||||
|
),
|
||||||
|
]
|
@ -32,9 +32,10 @@ import InvenTree.models
|
|||||||
import InvenTree.ready
|
import InvenTree.ready
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
|
|
||||||
|
import common.models
|
||||||
|
from common.notifications import trigger_notification
|
||||||
from plugin.events import trigger_event
|
from plugin.events import trigger_event
|
||||||
|
|
||||||
import common.notifications
|
|
||||||
import part.models
|
import part.models
|
||||||
import stock.models
|
import stock.models
|
||||||
import users.models
|
import users.models
|
||||||
@ -301,6 +302,14 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
help_text=_('Priority of this build order')
|
help_text=_('Priority of this build order')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
project_code = models.ForeignKey(
|
||||||
|
common.models.ProjectCode,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True, null=True,
|
||||||
|
verbose_name=_('Project Code'),
|
||||||
|
help_text=_('Project code for this build order'),
|
||||||
|
)
|
||||||
|
|
||||||
def sub_builds(self, cascade=True):
|
def sub_builds(self, cascade=True):
|
||||||
"""Return all Build Order objects under this one."""
|
"""Return all Build Order objects under this one."""
|
||||||
if cascade:
|
if cascade:
|
||||||
@ -547,7 +556,7 @@ class Build(MPTTModel, InvenTree.models.InvenTreeBarcodeMixin, InvenTree.models.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
common.notifications.trigger_notification(
|
trigger_notification(
|
||||||
build,
|
build,
|
||||||
'build.completed',
|
'build.completed',
|
||||||
targets=targets,
|
targets=targets,
|
||||||
|
@ -23,6 +23,7 @@ from InvenTree.status_codes import StockStatus
|
|||||||
from stock.models import generate_batch_code, StockItem, StockLocation
|
from stock.models import generate_batch_code, StockItem, StockLocation
|
||||||
from stock.serializers import StockItemSerializerBrief, LocationSerializer
|
from stock.serializers import StockItemSerializerBrief, LocationSerializer
|
||||||
|
|
||||||
|
from common.serializers import ProjectCodeSerializer
|
||||||
import part.filters
|
import part.filters
|
||||||
from part.serializers import BomItemSerializer, PartSerializer, PartBriefSerializer
|
from part.serializers import BomItemSerializer, PartSerializer, PartBriefSerializer
|
||||||
from users.serializers import OwnerSerializer
|
from users.serializers import OwnerSerializer
|
||||||
@ -49,6 +50,8 @@ class BuildSerializer(InvenTreeModelSerializer):
|
|||||||
'parent',
|
'parent',
|
||||||
'part',
|
'part',
|
||||||
'part_detail',
|
'part_detail',
|
||||||
|
'project_code',
|
||||||
|
'project_code_detail',
|
||||||
'overdue',
|
'overdue',
|
||||||
'reference',
|
'reference',
|
||||||
'sales_order',
|
'sales_order',
|
||||||
@ -90,6 +93,8 @@ class BuildSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
barcode_hash = serializers.CharField(read_only=True)
|
barcode_hash = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
project_code_detail = ProjectCodeSerializer(source='project_code', many=False, read_only=True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def annotate_queryset(queryset):
|
def annotate_queryset(queryset):
|
||||||
"""Add custom annotations to the BuildSerializer queryset, performing database queries as efficiently as possible.
|
"""Add custom annotations to the BuildSerializer queryset, performing database queries as efficiently as possible.
|
||||||
|
@ -108,6 +108,7 @@ src="{% static 'img/blank_image.png' %}"
|
|||||||
<td>{% trans "Build Description" %}</td>
|
<td>{% trans "Build Description" %}</td>
|
||||||
<td>{{ build.title }}</td>
|
<td>{{ build.title }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% include "project_code_data.html" with instance=build %}
|
||||||
{% include "barcode_data.html" with instance=build %}
|
{% include "barcode_data.html" with instance=build %}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -19,22 +19,15 @@ class TestForwardMigrations(MigratorTestCase):
|
|||||||
name='Widget',
|
name='Widget',
|
||||||
description='Buildable Part',
|
description='Buildable Part',
|
||||||
active=True,
|
active=True,
|
||||||
|
level=0, lft=0, rght=0, tree_id=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.assertRaises(TypeError):
|
|
||||||
# Cannot set the 'assembly' field as it hasn't been added to the db schema
|
|
||||||
Part.objects.create(
|
|
||||||
name='Blorb',
|
|
||||||
description='ABCDE',
|
|
||||||
assembly=True
|
|
||||||
)
|
|
||||||
|
|
||||||
Build = self.old_state.apps.get_model('build', 'build')
|
Build = self.old_state.apps.get_model('build', 'build')
|
||||||
|
|
||||||
Build.objects.create(
|
Build.objects.create(
|
||||||
part=buildable_part,
|
part=buildable_part,
|
||||||
title='A build of some stuff',
|
title='A build of some stuff',
|
||||||
quantity=50
|
quantity=50,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_items_exist(self):
|
def test_items_exist(self):
|
||||||
@ -67,7 +60,8 @@ class TestReferenceMigration(MigratorTestCase):
|
|||||||
|
|
||||||
part = Part.objects.create(
|
part = Part.objects.create(
|
||||||
name='Part',
|
name='Part',
|
||||||
description='A test part'
|
description='A test part',
|
||||||
|
level=0, lft=0, rght=0, tree_id=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
Build = self.old_state.apps.get_model('build', 'build')
|
Build = self.old_state.apps.get_model('build', 'build')
|
||||||
|
@ -525,7 +525,7 @@ settings_api_urls = [
|
|||||||
path(r'<int:pk>/', NotificationUserSettingsDetail.as_view(), name='api-notification-setting-detail'),
|
path(r'<int:pk>/', NotificationUserSettingsDetail.as_view(), name='api-notification-setting-detail'),
|
||||||
|
|
||||||
# Notification Settings List
|
# Notification Settings List
|
||||||
re_path(r'^.*$', NotificationUserSettingsList.as_view(), name='api-notifcation-setting-list'),
|
re_path(r'^.*$', NotificationUserSettingsList.as_view(), name='api-notification-setting-list'),
|
||||||
])),
|
])),
|
||||||
|
|
||||||
# Global settings
|
# Global settings
|
||||||
|
@ -8,8 +8,8 @@ from django.contrib.auth import get_user_model
|
|||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
import common.models
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
from common.models import NotificationEntry, NotificationMessage
|
|
||||||
from InvenTree.ready import isImportingData
|
from InvenTree.ready import isImportingData
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
from plugin.models import NotificationUserSetting, PluginConfig
|
from plugin.models import NotificationUserSetting, PluginConfig
|
||||||
@ -247,7 +247,7 @@ class UIMessageNotification(SingleNotificationMethod):
|
|||||||
|
|
||||||
def send(self, target):
|
def send(self, target):
|
||||||
"""Send a UI notification to a user."""
|
"""Send a UI notification to a user."""
|
||||||
NotificationMessage.objects.create(
|
common.models.NotificationMessage.objects.create(
|
||||||
target_object=self.obj,
|
target_object=self.obj,
|
||||||
source_object=target,
|
source_object=target,
|
||||||
user=target,
|
user=target,
|
||||||
@ -338,7 +338,7 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
|
|||||||
# Check if we have notified recently...
|
# Check if we have notified recently...
|
||||||
delta = timedelta(days=1)
|
delta = timedelta(days=1)
|
||||||
|
|
||||||
if NotificationEntry.check_recent(category, obj_ref_value, delta):
|
if common.models.NotificationEntry.check_recent(category, obj_ref_value, delta):
|
||||||
logger.info(f"Notification '{category}' has recently been sent for '{str(obj)}' - SKIPPING")
|
logger.info(f"Notification '{category}' has recently been sent for '{str(obj)}' - SKIPPING")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -398,7 +398,7 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
|
|||||||
logger.error(error)
|
logger.error(error)
|
||||||
|
|
||||||
# Set delivery flag
|
# Set delivery flag
|
||||||
NotificationEntry.notify(category, obj_ref_value)
|
common.models.NotificationEntry.notify(category, obj_ref_value)
|
||||||
else:
|
else:
|
||||||
logger.info(f"No possible users for notification '{category}'")
|
logger.info(f"No possible users for notification '{category}'")
|
||||||
|
|
||||||
|
@ -6,9 +6,7 @@ from django.urls import reverse
|
|||||||
from flags.state import flag_state
|
from flags.state import flag_state
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from common.models import (InvenTreeSetting, InvenTreeUserSetting,
|
import common.models as common_models
|
||||||
NewsFeedEntry, NotesImage, NotificationMessage,
|
|
||||||
ProjectCode)
|
|
||||||
from InvenTree.helpers import get_objectreference
|
from InvenTree.helpers import get_objectreference
|
||||||
from InvenTree.helpers_model import construct_absolute_url
|
from InvenTree.helpers_model import construct_absolute_url
|
||||||
from InvenTree.serializers import (InvenTreeImageSerializerField,
|
from InvenTree.serializers import (InvenTreeImageSerializerField,
|
||||||
@ -64,7 +62,7 @@ class GlobalSettingsSerializer(SettingsSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
"""Meta options for GlobalSettingsSerializer."""
|
"""Meta options for GlobalSettingsSerializer."""
|
||||||
|
|
||||||
model = InvenTreeSetting
|
model = common_models.InvenTreeSetting
|
||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
'key',
|
'key',
|
||||||
@ -85,7 +83,7 @@ class UserSettingsSerializer(SettingsSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
"""Meta options for UserSettingsSerializer."""
|
"""Meta options for UserSettingsSerializer."""
|
||||||
|
|
||||||
model = InvenTreeUserSetting
|
model = common_models.InvenTreeUserSetting
|
||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
'key',
|
'key',
|
||||||
@ -148,7 +146,7 @@ class NotificationMessageSerializer(InvenTreeModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
"""Meta options for NotificationMessageSerializer."""
|
"""Meta options for NotificationMessageSerializer."""
|
||||||
|
|
||||||
model = NotificationMessage
|
model = common_models.NotificationMessage
|
||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
'target',
|
'target',
|
||||||
@ -209,7 +207,7 @@ class NewsFeedEntrySerializer(InvenTreeModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
"""Meta options for NewsFeedEntrySerializer."""
|
"""Meta options for NewsFeedEntrySerializer."""
|
||||||
|
|
||||||
model = NewsFeedEntry
|
model = common_models.NewsFeedEntry
|
||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
'feed_id',
|
'feed_id',
|
||||||
@ -243,7 +241,7 @@ class NotesImageSerializer(InvenTreeModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
"""Meta options for NotesImageSerializer."""
|
"""Meta options for NotesImageSerializer."""
|
||||||
|
|
||||||
model = NotesImage
|
model = common_models.NotesImage
|
||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
'image',
|
'image',
|
||||||
@ -265,7 +263,7 @@ class ProjectCodeSerializer(InvenTreeModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
"""Meta options for ProjectCodeSerializer."""
|
"""Meta options for ProjectCodeSerializer."""
|
||||||
|
|
||||||
model = ProjectCode
|
model = common_models.ProjectCode
|
||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
'code',
|
'code',
|
||||||
|
@ -226,7 +226,7 @@ class SettingsTest(InvenTreeTestCase):
|
|||||||
|
|
||||||
cache.clear()
|
cache.clear()
|
||||||
|
|
||||||
# Generate a number of new usesr
|
# Generate a number of new users
|
||||||
for idx in range(5):
|
for idx in range(5):
|
||||||
get_user_model().objects.create(
|
get_user_model().objects.create(
|
||||||
username=f"User_{idx}",
|
username=f"User_{idx}",
|
||||||
@ -417,7 +417,7 @@ class UserSettingsApiTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
self.assertTrue(str2bool(response.data['value']))
|
self.assertTrue(str2bool(response.data['value']))
|
||||||
|
|
||||||
# Assign some falsey values
|
# Assign some false(ish) values
|
||||||
for v in ['false', False, '0', 'n', 'FalSe']:
|
for v in ['false', False, '0', 'n', 'FalSe']:
|
||||||
self.patch(
|
self.patch(
|
||||||
url,
|
url,
|
||||||
@ -535,7 +535,7 @@ class NotificationUserSettingsApiTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
def test_api_list(self):
|
def test_api_list(self):
|
||||||
"""Test list URL."""
|
"""Test list URL."""
|
||||||
url = reverse('api-notifcation-setting-list')
|
url = reverse('api-notification-setting-list')
|
||||||
|
|
||||||
self.get(url, expected_code=200)
|
self.get(url, expected_code=200)
|
||||||
|
|
||||||
@ -583,7 +583,7 @@ class PluginSettingsApiTest(PluginMixin, InvenTreeAPITestCase):
|
|||||||
|
|
||||||
# Failure mode tests
|
# Failure mode tests
|
||||||
|
|
||||||
# Non - exsistant plugin
|
# Non-existent plugin
|
||||||
url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'doesnotexist', 'key': 'doesnotmatter'})
|
url = reverse('api-plugin-setting-detail', kwargs={'plugin': 'doesnotexist', 'key': 'doesnotmatter'})
|
||||||
response = self.get(url, expected_code=404)
|
response = self.get(url, expected_code=404)
|
||||||
self.assertIn("Plugin 'doesnotexist' not installed", str(response.data))
|
self.assertIn("Plugin 'doesnotexist' not installed", str(response.data))
|
||||||
@ -729,7 +729,7 @@ class WebhookMessageTests(TestCase):
|
|||||||
|
|
||||||
|
|
||||||
class NotificationTest(InvenTreeAPITestCase):
|
class NotificationTest(InvenTreeAPITestCase):
|
||||||
"""Tests for NotificationEntriy."""
|
"""Tests for NotificationEntry."""
|
||||||
|
|
||||||
fixtures = [
|
fixtures = [
|
||||||
'users',
|
'users',
|
||||||
@ -785,7 +785,7 @@ class NotificationTest(InvenTreeAPITestCase):
|
|||||||
messages = NotificationMessage.objects.all()
|
messages = NotificationMessage.objects.all()
|
||||||
|
|
||||||
# As there are three staff users (including the 'test' user) we expect 30 notifications
|
# As there are three staff users (including the 'test' user) we expect 30 notifications
|
||||||
# However, one user is marked as i nactive
|
# However, one user is marked as inactive
|
||||||
self.assertEqual(messages.count(), 20)
|
self.assertEqual(messages.count(), 20)
|
||||||
|
|
||||||
# Only 10 messages related to *this* user
|
# Only 10 messages related to *this* user
|
||||||
|
@ -468,7 +468,7 @@ class SupplierPartTest(InvenTreeAPITestCase):
|
|||||||
self.assertIsNone(sp.availability_updated)
|
self.assertIsNone(sp.availability_updated)
|
||||||
self.assertEqual(sp.available, 0)
|
self.assertEqual(sp.available, 0)
|
||||||
|
|
||||||
# Now, *update* the availabile quantity via the API
|
# Now, *update* the available quantity via the API
|
||||||
self.patch(
|
self.patch(
|
||||||
reverse('api-supplier-part-detail', kwargs={'pk': sp.pk}),
|
reverse('api-supplier-part-detail', kwargs={'pk': sp.pk}),
|
||||||
{
|
{
|
||||||
|
@ -48,7 +48,8 @@ class TestManufacturerField(MigratorTestCase):
|
|||||||
# Create an initial part
|
# Create an initial part
|
||||||
part = Part.objects.create(
|
part = Part.objects.create(
|
||||||
name='Screw',
|
name='Screw',
|
||||||
description='A single screw'
|
description='A single screw',
|
||||||
|
level=0, tree_id=0, lft=0, rght=0
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create a company to act as the supplier
|
# Create a company to act as the supplier
|
||||||
|
@ -13,7 +13,7 @@ from rest_framework import status
|
|||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from common.models import InvenTreeSetting, ProjectCode
|
import common.models as common_models
|
||||||
from common.settings import settings
|
from common.settings import settings
|
||||||
from company.models import SupplierPart
|
from company.models import SupplierPart
|
||||||
from generic.states import StatusView
|
from generic.states import StatusView
|
||||||
@ -139,7 +139,7 @@ class OrderFilter(rest_filters.FilterSet):
|
|||||||
return queryset.exclude(status__in=self.Meta.model.get_status_class().OPEN)
|
return queryset.exclude(status__in=self.Meta.model.get_status_class().OPEN)
|
||||||
|
|
||||||
project_code = rest_filters.ModelChoiceFilter(
|
project_code = rest_filters.ModelChoiceFilter(
|
||||||
queryset=ProjectCode.objects.all(),
|
queryset=common_models.ProjectCode.objects.all(),
|
||||||
field_name='project_code'
|
field_name='project_code'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1457,7 +1457,7 @@ class OrderCalendarExport(ICalFeed):
|
|||||||
else:
|
else:
|
||||||
ordertype_title = _('Unknown')
|
ordertype_title = _('Unknown')
|
||||||
|
|
||||||
return f'{InvenTreeSetting.get_setting("INVENTREE_COMPANY_NAME")} {ordertype_title}'
|
return f'{common_models.InvenTreeSetting.get_setting("INVENTREE_COMPANY_NAME")} {ordertype_title}'
|
||||||
|
|
||||||
def product_id(self, obj):
|
def product_id(self, obj):
|
||||||
"""Return calendar product id."""
|
"""Return calendar product id."""
|
||||||
|
@ -22,6 +22,7 @@ from djmoney.contrib.exchange.models import convert_money
|
|||||||
from djmoney.money import Money
|
from djmoney.money import Money
|
||||||
from mptt.models import TreeForeignKey
|
from mptt.models import TreeForeignKey
|
||||||
|
|
||||||
|
import common.models as common_models
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.ready
|
import InvenTree.ready
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
@ -29,7 +30,6 @@ import InvenTree.validators
|
|||||||
import order.validators
|
import order.validators
|
||||||
import stock.models
|
import stock.models
|
||||||
import users.models as UserModels
|
import users.models as UserModels
|
||||||
from common.models import ProjectCode
|
|
||||||
from common.notifications import InvenTreeNotificationBodies
|
from common.notifications import InvenTreeNotificationBodies
|
||||||
from common.settings import currency_code_default
|
from common.settings import currency_code_default
|
||||||
from company.models import Company, Contact, SupplierPart
|
from company.models import Company, Contact, SupplierPart
|
||||||
@ -231,7 +231,11 @@ class Order(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, Reference
|
|||||||
|
|
||||||
description = models.CharField(max_length=250, blank=True, verbose_name=_('Description'), help_text=_('Order description (optional)'))
|
description = models.CharField(max_length=250, blank=True, verbose_name=_('Description'), help_text=_('Order description (optional)'))
|
||||||
|
|
||||||
project_code = models.ForeignKey(ProjectCode, on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_('Project Code'), help_text=_('Select project code for this order'))
|
project_code = models.ForeignKey(
|
||||||
|
common_models.ProjectCode, on_delete=models.SET_NULL,
|
||||||
|
blank=True, null=True,
|
||||||
|
verbose_name=_('Project Code'), help_text=_('Select project code for this order')
|
||||||
|
)
|
||||||
|
|
||||||
link = InvenTreeURLField(blank=True, verbose_name=_('Link'), help_text=_('Link to external page'))
|
link = InvenTreeURLField(blank=True, verbose_name=_('Link'), help_text=_('Link to external page'))
|
||||||
|
|
||||||
|
@ -13,11 +13,11 @@ from rest_framework import serializers
|
|||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
from sql_util.utils import SubqueryCount
|
from sql_util.utils import SubqueryCount
|
||||||
|
|
||||||
import common.serializers
|
|
||||||
import order.models
|
import order.models
|
||||||
import part.filters
|
import part.filters
|
||||||
import stock.models
|
import stock.models
|
||||||
import stock.serializers
|
import stock.serializers
|
||||||
|
from common.serializers import ProjectCodeSerializer
|
||||||
from company.serializers import (CompanyBriefSerializer, ContactSerializer,
|
from company.serializers import (CompanyBriefSerializer, ContactSerializer,
|
||||||
SupplierPartSerializer)
|
SupplierPartSerializer)
|
||||||
from InvenTree.helpers import (extract_serial_numbers, hash_barcode, normalize,
|
from InvenTree.helpers import (extract_serial_numbers, hash_barcode, normalize,
|
||||||
@ -73,7 +73,7 @@ class AbstractOrderSerializer(serializers.Serializer):
|
|||||||
responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False)
|
responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False)
|
||||||
|
|
||||||
# Detail for project code field
|
# Detail for project code field
|
||||||
project_code_detail = common.serializers.ProjectCodeSerializer(source='project_code', read_only=True, many=False)
|
project_code_detail = ProjectCodeSerializer(source='project_code', read_only=True, many=False)
|
||||||
|
|
||||||
# Boolean field indicating if this order is overdue (Note: must be annotated)
|
# Boolean field indicating if this order is overdue (Note: must be annotated)
|
||||||
overdue = serializers.BooleanField(required=False, read_only=True)
|
overdue = serializers.BooleanField(required=False, read_only=True)
|
||||||
|
@ -54,7 +54,7 @@ class PluginDetailAPITest(PluginMixin, InvenTreeAPITestCase):
|
|||||||
).data
|
).data
|
||||||
self.assertEqual(data['success'], True)
|
self.assertEqual(data['success'], True)
|
||||||
|
|
||||||
# valid - github url and packagename
|
# valid - github url and package name
|
||||||
data = self.post(
|
data = self.post(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
FullCalendar,
|
FullCalendar,
|
||||||
getFormFieldValue,
|
getFormFieldValue,
|
||||||
getTableData,
|
getTableData,
|
||||||
|
global_settings,
|
||||||
handleFormErrors,
|
handleFormErrors,
|
||||||
handleFormSuccess,
|
handleFormSuccess,
|
||||||
imageHoverIcon,
|
imageHoverIcon,
|
||||||
@ -64,7 +65,7 @@
|
|||||||
|
|
||||||
|
|
||||||
function buildFormFields() {
|
function buildFormFields() {
|
||||||
return {
|
let fields = {
|
||||||
reference: {
|
reference: {
|
||||||
icon: 'fa-hashtag',
|
icon: 'fa-hashtag',
|
||||||
},
|
},
|
||||||
@ -76,6 +77,9 @@ function buildFormFields() {
|
|||||||
},
|
},
|
||||||
title: {},
|
title: {},
|
||||||
quantity: {},
|
quantity: {},
|
||||||
|
project_code: {
|
||||||
|
icon: 'fa-list',
|
||||||
|
},
|
||||||
priority: {},
|
priority: {},
|
||||||
parent: {
|
parent: {
|
||||||
filters: {
|
filters: {
|
||||||
@ -111,6 +115,12 @@ function buildFormFields() {
|
|||||||
icon: 'fa-users',
|
icon: 'fa-users',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!global_settings.PROJECT_CODES_ENABLED) {
|
||||||
|
delete fields.project_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -2020,6 +2030,18 @@ function loadBuildTable(table, options) {
|
|||||||
title: '{% trans "Description" %}',
|
title: '{% trans "Description" %}',
|
||||||
switchable: true,
|
switchable: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'project_code',
|
||||||
|
title: '{% trans "Project Code" %}',
|
||||||
|
sortable: true,
|
||||||
|
switchable: global_settings.PROJECT_CODES_ENABLED,
|
||||||
|
visible: global_settings.PROJECT_CODES_ENABLED,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
if (row.project_code_detail) {
|
||||||
|
return `<span title='${row.project_code_detail.description}'>${row.project_code_detail.code}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'priority',
|
field: 'priority',
|
||||||
title: '{% trans "Priority" %}',
|
title: '{% trans "Priority" %}',
|
||||||
|
@ -440,7 +440,7 @@ function getPluginTableFilters() {
|
|||||||
// Return a dictionary of filters for the "build" table
|
// Return a dictionary of filters for the "build" table
|
||||||
function getBuildTableFilters() {
|
function getBuildTableFilters() {
|
||||||
|
|
||||||
return {
|
let filters = {
|
||||||
status: {
|
status: {
|
||||||
title: '{% trans "Build status" %}',
|
title: '{% trans "Build status" %}',
|
||||||
options: buildCodes,
|
options: buildCodes,
|
||||||
@ -477,6 +477,13 @@ function getBuildTableFilters() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (global_settings.PROJECT_CODES_ENABLED) {
|
||||||
|
filters['has_project_code'] = constructHasProjectCodeFilter();
|
||||||
|
filters['project_code'] = constructProjectCodeFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user