2
0
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:
Oliver 2023-06-14 11:23:35 +10:00 committed by GitHub
parent c8365ccd0c
commit 00bb740216
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 151 additions and 62 deletions

View File

@ -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

View File

@ -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')

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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',
] ]

View 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'),
),
]

View File

@ -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,

View File

@ -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.

View File

@ -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>

View File

@ -19,14 +19,7 @@ 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')
@ -34,7 +27,7 @@ class TestForwardMigrations(MigratorTestCase):
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')

View File

@ -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

View File

@ -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}'")

View File

@ -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',

View File

@ -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

View File

@ -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}),
{ {

View File

@ -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

View File

@ -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."""

View File

@ -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'))

View File

@ -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)

View File

@ -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,
{ {

View File

@ -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" %}',

View File

@ -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;
} }