mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 20:45:44 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into matmair/issue6281
This commit is contained in:
15
docs/docs/build/build.md
vendored
15
docs/docs/build/build.md
vendored
@ -49,6 +49,7 @@ The following parameters are available for each Build Order, and can be edited b
|
|||||||
| Sales Order | Link to a *Sales Order* to which the build outputs will be allocated |
|
| Sales Order | Link to a *Sales Order* to which the build outputs will be allocated |
|
||||||
| Source Location | Stock location to source stock items from (blank = all locations) |
|
| Source Location | Stock location to source stock items from (blank = all locations) |
|
||||||
| Destination Location | Stock location where the build outputs will be located |
|
| Destination Location | Stock location where the build outputs will be located |
|
||||||
|
| Start Date | The scheduled start date for the build |
|
||||||
| Target Date | Target date for build completion |
|
| Target Date | Target date for build completion |
|
||||||
| Responsible | User (or group of users) who is responsible for the build |
|
| Responsible | User (or group of users) who is responsible for the build |
|
||||||
| External Link | Link to external webpage |
|
| External Link | Link to external webpage |
|
||||||
@ -262,12 +263,18 @@ The `Cancel Build` form will be displayed, click on the confirmation switch then
|
|||||||
!!! warning "Cancelled Build"
|
!!! warning "Cancelled Build"
|
||||||
**A cancelled build cannot be re-opened**. Make sure to use the cancel option only if you are certain that the build won't be processed.
|
**A cancelled build cannot be re-opened**. Make sure to use the cancel option only if you are certain that the build won't be processed.
|
||||||
|
|
||||||
## Overdue Builds
|
## Build Scheduling
|
||||||
|
|
||||||
Build orders may (optionally) have a target complete date specified. If this date is reached but the build order remains incomplete, then the build is considered *overdue*.
|
### Start Date
|
||||||
|
|
||||||
|
Build orders can be optionally scheduled to *start* at a specified date. This may be useful for planning production schedules.
|
||||||
|
|
||||||
|
### Overdue Builds
|
||||||
|
|
||||||
|
Build orders may (optionally) have a target completion date specified. If this date is reached but the build order remains incomplete, then the build is considered *overdue*.
|
||||||
|
|
||||||
|
This can be useful for tracking production delays, and can be used to generate reports on build order performance.
|
||||||
|
|
||||||
- Builds can be filtered by overdue status in the build list
|
|
||||||
- Overdue builds will be displayed on the home page
|
|
||||||
|
|
||||||
## Build Order Settings
|
## Build Order Settings
|
||||||
|
|
||||||
|
@ -11,6 +11,10 @@ v303 - 2025-01-20 : https://github.com/inventree/InvenTree/pull/6293
|
|||||||
- Removes a considerable amount of old auth endpoints
|
- Removes a considerable amount of old auth endpoints
|
||||||
- Introduces allauth based REST API
|
- Introduces allauth based REST API
|
||||||
|
|
||||||
|
v303 - 2025-01-20 - https://github.com/inventree/InvenTree/pull/8915
|
||||||
|
- Adds "start_date" field to Build model and API endpoints
|
||||||
|
- Adds additional API filtering and sorting options for Build list
|
||||||
|
|
||||||
v302 - 2025-01-18 - https://github.com/inventree/InvenTree/pull/8905
|
v302 - 2025-01-18 - https://github.com/inventree/InvenTree/pull/8905
|
||||||
- Fix schema definition on the /label/print endpoint
|
- Fix schema definition on the /label/print endpoint
|
||||||
|
|
||||||
|
@ -188,6 +188,30 @@ class BuildFilter(rest_filters.FilterSet):
|
|||||||
label=_('Created after'), field_name='creation_date', lookup_expr='gt'
|
label=_('Created after'), field_name='creation_date', lookup_expr='gt'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
has_start_date = rest_filters.BooleanFilter(
|
||||||
|
label=_('Has start date'), method='filter_has_start_date'
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_has_start_date(self, queryset, name, value):
|
||||||
|
"""Filter by whether or not the order has a start date."""
|
||||||
|
return queryset.filter(start_date__isnull=not str2bool(value))
|
||||||
|
|
||||||
|
start_date_before = InvenTreeDateFilter(
|
||||||
|
label=_('Start date before'), field_name='start_date', lookup_expr='lt'
|
||||||
|
)
|
||||||
|
|
||||||
|
start_date_after = InvenTreeDateFilter(
|
||||||
|
label=_('Start date after'), field_name='start_date', lookup_expr='gt'
|
||||||
|
)
|
||||||
|
|
||||||
|
has_target_date = rest_filters.BooleanFilter(
|
||||||
|
label=_('Has target date'), method='filter_has_target_date'
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_has_target_date(self, queryset, name, value):
|
||||||
|
"""Filter by whether or not the order has a target date."""
|
||||||
|
return queryset.filter(target_date__isnull=not str2bool(value))
|
||||||
|
|
||||||
target_date_before = InvenTreeDateFilter(
|
target_date_before = InvenTreeDateFilter(
|
||||||
label=_('Target date before'), field_name='target_date', lookup_expr='lt'
|
label=_('Target date before'), field_name='target_date', lookup_expr='lt'
|
||||||
)
|
)
|
||||||
@ -244,6 +268,7 @@ class BuildList(DataExportViewMixin, BuildMixin, ListCreateAPI):
|
|||||||
'part__name',
|
'part__name',
|
||||||
'status',
|
'status',
|
||||||
'creation_date',
|
'creation_date',
|
||||||
|
'start_date',
|
||||||
'target_date',
|
'target_date',
|
||||||
'completion_date',
|
'completion_date',
|
||||||
'quantity',
|
'quantity',
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.18 on 2025-01-20 02:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('build', '0053_alter_build_part'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='build',
|
||||||
|
name='start_date',
|
||||||
|
field=models.DateField(blank=True, help_text='Scheduled start date for this build order', null=True, verbose_name='Build start date'),
|
||||||
|
),
|
||||||
|
]
|
@ -67,7 +67,7 @@ class Build(
|
|||||||
Attributes:
|
Attributes:
|
||||||
part: The part to be built (from component BOM items)
|
part: The part to be built (from component BOM items)
|
||||||
reference: Build order reference (required, must be unique)
|
reference: Build order reference (required, must be unique)
|
||||||
title: Brief title describing the build (required)
|
title: Brief title describing the build (optional)
|
||||||
quantity: Number of units to be built
|
quantity: Number of units to be built
|
||||||
parent: Reference to a Build object for which this Build is required
|
parent: Reference to a Build object for which this Build is required
|
||||||
sales_order: References to a SalesOrder object for which this Build is required (e.g. the output of this build will be used to fulfil a sales order)
|
sales_order: References to a SalesOrder object for which this Build is required (e.g. the output of this build will be used to fulfil a sales order)
|
||||||
@ -178,6 +178,12 @@ class Build(
|
|||||||
if self.has_field_changed('part'):
|
if self.has_field_changed('part'):
|
||||||
raise ValidationError({'part': _('Build order part cannot be changed')})
|
raise ValidationError({'part': _('Build order part cannot be changed')})
|
||||||
|
|
||||||
|
# Target date should be *after* the start date
|
||||||
|
if self.start_date and self.target_date and self.start_date > self.target_date:
|
||||||
|
raise ValidationError({
|
||||||
|
'target_date': _('Target date must be after start date')
|
||||||
|
})
|
||||||
|
|
||||||
def report_context(self) -> dict:
|
def report_context(self) -> dict:
|
||||||
"""Generate custom report context data."""
|
"""Generate custom report context data."""
|
||||||
return {
|
return {
|
||||||
@ -344,6 +350,13 @@ class Build(
|
|||||||
auto_now_add=True, editable=False, verbose_name=_('Creation Date')
|
auto_now_add=True, editable=False, verbose_name=_('Creation Date')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
start_date = models.DateField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name=_('Build start date'),
|
||||||
|
help_text=_('Scheduled start date for this build order'),
|
||||||
|
)
|
||||||
|
|
||||||
target_date = models.DateField(
|
target_date = models.DateField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
@ -81,6 +81,7 @@ class BuildSerializer(
|
|||||||
'reference',
|
'reference',
|
||||||
'sales_order',
|
'sales_order',
|
||||||
'quantity',
|
'quantity',
|
||||||
|
'start_date',
|
||||||
'status',
|
'status',
|
||||||
'status_text',
|
'status_text',
|
||||||
'status_custom_key',
|
'status_custom_key',
|
||||||
|
@ -1003,6 +1003,12 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
|||||||
'validator': bool,
|
'validator': bool,
|
||||||
'after_save': reload_plugin_registry,
|
'after_save': reload_plugin_registry,
|
||||||
},
|
},
|
||||||
|
'PROJECT_CODES_ENABLED': {
|
||||||
|
'name': _('Enable project codes'),
|
||||||
|
'description': _('Enable project codes for tracking projects'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
'STOCKTAKE_ENABLE': {
|
'STOCKTAKE_ENABLE': {
|
||||||
'name': _('Stocktake Functionality'),
|
'name': _('Stocktake Functionality'),
|
||||||
'description': _(
|
'description': _(
|
||||||
|
@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
import tablib
|
import tablib
|
||||||
from rest_framework import fields, serializers
|
from rest_framework import fields, serializers
|
||||||
|
from taggit.serializers import TagListSerializerField
|
||||||
|
|
||||||
import importer.operations
|
import importer.operations
|
||||||
from InvenTree.helpers import DownloadFile, GetExportFormats, current_date
|
from InvenTree.helpers import DownloadFile, GetExportFormats, current_date
|
||||||
@ -81,6 +82,11 @@ class DataImportSerializerMixin:
|
|||||||
if issubclass(field.__class__, fields.FileField):
|
if issubclass(field.__class__, fields.FileField):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Skip tags fields
|
||||||
|
# TODO: Implement tag field support
|
||||||
|
if issubclass(field.__class__, TagListSerializerField):
|
||||||
|
continue
|
||||||
|
|
||||||
importable_fields[name] = field
|
importable_fields[name] = field
|
||||||
|
|
||||||
return importable_fields
|
return importable_fields
|
||||||
|
@ -683,7 +683,7 @@ class PartSerializer(
|
|||||||
Used when displaying all details of a single component.
|
Used when displaying all details of a single component.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import_exclude_fields = ['duplicate', 'tags']
|
import_exclude_fields = ['duplicate']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass defining serializer fields."""
|
"""Metaclass defining serializer fields."""
|
||||||
|
@ -1985,9 +1985,18 @@ class StockItem(
|
|||||||
Returns:
|
Returns:
|
||||||
The new StockItem object
|
The new StockItem object
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValidationError: If the stock item cannot be split
|
||||||
|
|
||||||
- The provided quantity will be subtracted from this item and given to the new one.
|
- The provided quantity will be subtracted from this item and given to the new one.
|
||||||
- The new item will have a different StockItem ID, while this will remain the same.
|
- The new item will have a different StockItem ID, while this will remain the same.
|
||||||
"""
|
"""
|
||||||
|
# Run initial checks to test if the stock item can actually be "split"
|
||||||
|
|
||||||
|
# Cannot split a stock item which is in production
|
||||||
|
if self.is_building:
|
||||||
|
raise ValidationError(_('Stock item is currently in production'))
|
||||||
|
|
||||||
notes = kwargs.get('notes', '')
|
notes = kwargs.get('notes', '')
|
||||||
|
|
||||||
# Do not split a serialized part
|
# Do not split a serialized part
|
||||||
|
@ -344,7 +344,7 @@ class StockItemSerializer(
|
|||||||
|
|
||||||
export_only_fields = ['part_pricing_min', 'part_pricing_max']
|
export_only_fields = ['part_pricing_min', 'part_pricing_max']
|
||||||
|
|
||||||
import_exclude_fields = ['use_pack_size', 'tags']
|
import_exclude_fields = ['use_pack_size']
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass options."""
|
"""Metaclass options."""
|
||||||
@ -1142,7 +1142,7 @@ class LocationSerializer(
|
|||||||
):
|
):
|
||||||
"""Detailed information about a stock location."""
|
"""Detailed information about a stock location."""
|
||||||
|
|
||||||
import_exclude_fields = ['tags']
|
import_exclude_fields = []
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Metaclass options."""
|
"""Metaclass options."""
|
||||||
@ -1565,18 +1565,18 @@ class StockAdjustmentItemSerializer(serializers.Serializer):
|
|||||||
help_text=_('StockItem primary key value'),
|
help_text=_('StockItem primary key value'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_pk(self, pk):
|
def validate_pk(self, stock_item: StockItem) -> StockItem:
|
||||||
"""Ensure the stock item is valid."""
|
"""Ensure the stock item is valid."""
|
||||||
allow_out_of_stock_transfer = get_global_setting(
|
allow_out_of_stock_transfer = get_global_setting(
|
||||||
'STOCK_ALLOW_OUT_OF_STOCK_TRANSFER', backup_value=False, cache=False
|
'STOCK_ALLOW_OUT_OF_STOCK_TRANSFER', backup_value=False, cache=False
|
||||||
)
|
)
|
||||||
|
|
||||||
if not allow_out_of_stock_transfer and not pk.is_in_stock(
|
if not allow_out_of_stock_transfer and not stock_item.is_in_stock(
|
||||||
check_status=False, check_quantity=False
|
check_status=False, check_quantity=False
|
||||||
):
|
):
|
||||||
raise ValidationError(_('Stock item is not in stock'))
|
raise ValidationError(_('Stock item is not in stock'))
|
||||||
|
|
||||||
return pk
|
return stock_item
|
||||||
|
|
||||||
quantity = serializers.DecimalField(
|
quantity = serializers.DecimalField(
|
||||||
max_digits=15, decimal_places=5, min_value=Decimal(0), required=True
|
max_digits=15, decimal_places=5, min_value=Decimal(0), required=True
|
||||||
|
@ -11,14 +11,14 @@
|
|||||||
"compile": "lingui compile --typescript"
|
"compile": "lingui compile --typescript"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.18.1",
|
"@codemirror/autocomplete": "6.18.4",
|
||||||
"@codemirror/lang-liquid": "^6.2.1",
|
"@codemirror/lang-liquid": "6.2.2",
|
||||||
"@codemirror/language": "^6.10.3",
|
"@codemirror/language": "6.10.8",
|
||||||
"@codemirror/lint": "^6.8.1",
|
"@codemirror/lint": "6.8.4",
|
||||||
"@codemirror/search": "^6.5.6",
|
"@codemirror/search": "6.5.8",
|
||||||
"@codemirror/state": "^6.4.1",
|
"@codemirror/state": "6.5.1",
|
||||||
"@codemirror/theme-one-dark": "^6.1.2",
|
"@codemirror/theme-one-dark": "6.1.2",
|
||||||
"@codemirror/view": "^6.33.0",
|
"@codemirror/view": "6.36.2",
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.13.3",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
"@fortawesome/fontawesome-svg-core": "^6.6.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
"@fortawesome/free-regular-svg-icons": "^6.6.0",
|
||||||
@ -26,58 +26,58 @@
|
|||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
"@lingui/core": "^4.11.4",
|
"@lingui/core": "^4.11.4",
|
||||||
"@lingui/react": "^4.11.4",
|
"@lingui/react": "^4.11.4",
|
||||||
"@mantine/carousel": "^7.12.2",
|
"@mantine/carousel": "^7.16.0",
|
||||||
"@mantine/charts": "^7.12.2",
|
"@mantine/charts": "^7.16.0",
|
||||||
"@mantine/core": "^7.12.2",
|
"@mantine/core": "^7.16.0",
|
||||||
"@mantine/dates": "^7.12.2",
|
"@mantine/dates": "^7.16.0",
|
||||||
"@mantine/dropzone": "^7.12.2",
|
"@mantine/dropzone": "^7.16.0",
|
||||||
"@mantine/form": "^7.12.2",
|
"@mantine/form": "^7.16.0",
|
||||||
"@mantine/hooks": "^7.12.2",
|
"@mantine/hooks": "^7.16.0",
|
||||||
"@mantine/modals": "^7.12.2",
|
"@mantine/modals": "^7.16.0",
|
||||||
"@mantine/notifications": "^7.12.2",
|
"@mantine/notifications": "^7.16.0",
|
||||||
"@mantine/spotlight": "^7.12.2",
|
"@mantine/spotlight": "^7.16.0",
|
||||||
"@mantine/vanilla-extract": "^7.12.2",
|
"@mantine/vanilla-extract": "^7.16.0",
|
||||||
"@sentry/react": "^8.43.0",
|
"@sentry/react": "^8.43.0",
|
||||||
"@tabler/icons-react": "^3.17.0",
|
"@tabler/icons-react": "^3.17.0",
|
||||||
"@tanstack/react-query": "^5.56.2",
|
"@tanstack/react-query": "^5.56.2",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"@uiw/codemirror-theme-vscode": "^4.23.3",
|
"@uiw/codemirror-theme-vscode": "4.23.7",
|
||||||
"@uiw/react-codemirror": "^4.23.3",
|
"@uiw/react-codemirror": "4.23.7",
|
||||||
"@uiw/react-split": "^5.9.3",
|
"@uiw/react-split": "^5.9.3",
|
||||||
"@vanilla-extract/css": "^1.15.5",
|
"@vanilla-extract/css": "^1.17.0",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.9",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "6.0.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"dompurify": "^3.1.7",
|
"dompurify": "^3.1.7",
|
||||||
"easymde": "^2.18.0",
|
"easymde": "^2.18.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.5.2",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"html5-qrcode": "^2.3.8",
|
"html5-qrcode": "^2.3.8",
|
||||||
"mantine-contextmenu": "^7.11.3",
|
"mantine-contextmenu": "^7.15.3",
|
||||||
"mantine-datatable": "^7.12.4",
|
"mantine-datatable": "^7.15.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-grid-layout": "^1.4.4",
|
"react-grid-layout": "1.4.4",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-is": "^18.3.1",
|
"react-is": "^18.3.1",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.26.2",
|
||||||
"react-select": "^5.8.1",
|
"react-select": "^5.9.0",
|
||||||
"react-simplemde-editor": "^5.2.0",
|
"react-simplemde-editor": "^5.2.0",
|
||||||
"react-window": "^1.8.10",
|
"react-window": "1.8.10",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.15.0",
|
||||||
"styled-components": "^6.1.13",
|
"styled-components": "^6.1.14",
|
||||||
"zustand": "^4.5.5"
|
"zustand": "^4.5.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.26.0",
|
||||||
"@babel/preset-react": "^7.24.7",
|
"@babel/preset-react": "^7.26.3",
|
||||||
"@babel/preset-typescript": "^7.24.7",
|
"@babel/preset-typescript": "^7.26.0",
|
||||||
"@codecov/vite-plugin": "^1.2.0",
|
"@codecov/vite-plugin": "^1.7.0",
|
||||||
"@lingui/cli": "^4.11.4",
|
"@lingui/cli": "^4.11.4",
|
||||||
"@lingui/macro": "^4.11.4",
|
"@lingui/macro": "^4.11.4",
|
||||||
"@playwright/test": "^1.47.2",
|
"@playwright/test": "^1.49.1",
|
||||||
"@types/node": "^22.6.0",
|
"@types/node": "^22.6.0",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.3.8",
|
"@types/react": "^18.3.8",
|
||||||
@ -85,13 +85,13 @@
|
|||||||
"@types/react-grid-layout": "^1.3.5",
|
"@types/react-grid-layout": "^1.3.5",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"@vanilla-extract/vite-plugin": "^4.0.15",
|
"@vanilla-extract/vite-plugin": "^4.0.19",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"babel-plugin-macros": "^3.1.0",
|
"babel-plugin-macros": "^3.1.0",
|
||||||
"nyc": "^17.1.0",
|
"nyc": "^17.1.0",
|
||||||
"rollup-plugin-license": "^3.5.3",
|
"rollup-plugin-license": "^3.5.3",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.7.3",
|
||||||
"vite": "^5.4.7",
|
"vite": "^6.0.7",
|
||||||
"vite-plugin-babel-macros": "^1.0.6",
|
"vite-plugin-babel-macros": "^1.0.6",
|
||||||
"vite-plugin-istanbul": "^6.0.2"
|
"vite-plugin-istanbul": "^6.0.2"
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,12 @@ export default function DateField({
|
|||||||
fieldState: { error }
|
fieldState: { error }
|
||||||
} = controller;
|
} = controller;
|
||||||
|
|
||||||
const valueFormat =
|
const valueFormat = useMemo(() => {
|
||||||
definition.field_type == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss';
|
// Determine the format based on the field type
|
||||||
|
return definition.field_type == 'date'
|
||||||
|
? 'YYYY-MM-DD'
|
||||||
|
: 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
}, [definition.field_type]);
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(value: any) => {
|
(value: any) => {
|
||||||
@ -31,12 +35,13 @@ export default function DateField({
|
|||||||
if (value) {
|
if (value) {
|
||||||
value = value.toString();
|
value = value.toString();
|
||||||
value = dayjs(value).format(valueFormat);
|
value = dayjs(value).format(valueFormat);
|
||||||
|
value = value.toString().split('T')[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
field.onChange(value);
|
field.onChange(value);
|
||||||
definition.onValueChange?.(value);
|
definition.onValueChange?.(value);
|
||||||
},
|
},
|
||||||
[field.onChange, definition]
|
[field.onChange, definition, valueFormat]
|
||||||
);
|
);
|
||||||
|
|
||||||
const dateValue: Date | null = useMemo(() => {
|
const dateValue: Date | null = useMemo(() => {
|
||||||
@ -62,7 +67,7 @@ export default function DateField({
|
|||||||
ref={field.ref}
|
ref={field.ref}
|
||||||
type={undefined}
|
type={undefined}
|
||||||
error={definition.error ?? error?.message}
|
error={definition.error ?? error?.message}
|
||||||
value={dateValue ?? null}
|
value={dateValue}
|
||||||
clearable={!definition.required}
|
clearable={!definition.required}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
valueFormat={valueFormat}
|
valueFormat={valueFormat}
|
||||||
|
@ -29,6 +29,8 @@ import { type UserStateProps, useUserState } from '../../states/UserState';
|
|||||||
* @param navigate - The navigation function (see react-router-dom)
|
* @param navigate - The navigation function (see react-router-dom)
|
||||||
* @param theme - The current Mantine theme
|
* @param theme - The current Mantine theme
|
||||||
* @param colorScheme - The current Mantine color scheme (e.g. 'light' / 'dark')
|
* @param colorScheme - The current Mantine color scheme (e.g. 'light' / 'dark')
|
||||||
|
* @param host - The current host URL
|
||||||
|
* @param locale - The current locale string (e.g. 'en' / 'de')
|
||||||
* @param context - Any additional context data which may be passed to the plugin
|
* @param context - Any additional context data which may be passed to the plugin
|
||||||
*/
|
*/
|
||||||
export type InvenTreeContext = {
|
export type InvenTreeContext = {
|
||||||
@ -38,6 +40,7 @@ export type InvenTreeContext = {
|
|||||||
userSettings: SettingsStateProps;
|
userSettings: SettingsStateProps;
|
||||||
globalSettings: SettingsStateProps;
|
globalSettings: SettingsStateProps;
|
||||||
host: string;
|
host: string;
|
||||||
|
locale: string;
|
||||||
navigate: NavigateFunction;
|
navigate: NavigateFunction;
|
||||||
theme: MantineTheme;
|
theme: MantineTheme;
|
||||||
colorScheme: MantineColorScheme;
|
colorScheme: MantineColorScheme;
|
||||||
@ -45,7 +48,7 @@ export type InvenTreeContext = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useInvenTreeContext = () => {
|
export const useInvenTreeContext = () => {
|
||||||
const host = useLocalState((s) => s.host);
|
const [locale, host] = useLocalState((s) => [s.language, s.host]);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
const { colorScheme } = useMantineColorScheme();
|
const { colorScheme } = useMantineColorScheme();
|
||||||
@ -57,6 +60,7 @@ export const useInvenTreeContext = () => {
|
|||||||
return {
|
return {
|
||||||
user: user,
|
user: user,
|
||||||
host: host,
|
host: host,
|
||||||
|
locale: locale,
|
||||||
api: api,
|
api: api,
|
||||||
queryClient: queryClient,
|
queryClient: queryClient,
|
||||||
navigate: navigate,
|
navigate: navigate,
|
||||||
@ -69,6 +73,7 @@ export const useInvenTreeContext = () => {
|
|||||||
user,
|
user,
|
||||||
host,
|
host,
|
||||||
api,
|
api,
|
||||||
|
locale,
|
||||||
queryClient,
|
queryClient,
|
||||||
navigate,
|
navigate,
|
||||||
globalSettings,
|
globalSettings,
|
||||||
|
@ -101,6 +101,9 @@ export function useBuildOrderFields({
|
|||||||
value: batchCode,
|
value: batchCode,
|
||||||
onValueChange: (value: any) => setBatchCode(value)
|
onValueChange: (value: any) => setBatchCode(value)
|
||||||
},
|
},
|
||||||
|
start_date: {
|
||||||
|
icon: <IconCalendar />
|
||||||
|
},
|
||||||
target_date: {
|
target_date: {
|
||||||
icon: <IconCalendar />
|
icon: <IconCalendar />
|
||||||
},
|
},
|
||||||
@ -132,6 +135,10 @@ export function useBuildOrderFields({
|
|||||||
fields.create_child_builds = {};
|
fields.create_child_builds = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!globalSettings.isSet('PROJECT_CODES_ENABLED', true)) {
|
||||||
|
delete fields.project_code;
|
||||||
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}, [create, destination, batchCode, globalSettings]);
|
}, [create, destination, batchCode, globalSettings]);
|
||||||
}
|
}
|
||||||
|
@ -147,6 +147,8 @@ export function usePurchaseOrderFields({
|
|||||||
supplierId?: number;
|
supplierId?: number;
|
||||||
duplicateOrderId?: number;
|
duplicateOrderId?: number;
|
||||||
}): ApiFormFieldSet {
|
}): ApiFormFieldSet {
|
||||||
|
const globalSettings = useGlobalSettingsState();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const fields: ApiFormFieldSet = {
|
const fields: ApiFormFieldSet = {
|
||||||
reference: {
|
reference: {
|
||||||
@ -217,8 +219,12 @@ export function usePurchaseOrderFields({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!globalSettings.isSet('PROJECT_CODES_ENABLED', true)) {
|
||||||
|
delete fields.project_code;
|
||||||
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}, [duplicateOrderId, supplierId]);
|
}, [duplicateOrderId, supplierId, globalSettings]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -15,6 +15,7 @@ import { ApiEndpoints } from '../enums/ApiEndpoints';
|
|||||||
import { ModelType } from '../enums/ModelType';
|
import { ModelType } from '../enums/ModelType';
|
||||||
import { useCreateApiFormModal } from '../hooks/UseForm';
|
import { useCreateApiFormModal } from '../hooks/UseForm';
|
||||||
import { apiUrl } from '../states/ApiState';
|
import { apiUrl } from '../states/ApiState';
|
||||||
|
import { useGlobalSettingsState } from '../states/SettingsState';
|
||||||
import { StatusFilterOptions } from '../tables/Filter';
|
import { StatusFilterOptions } from '../tables/Filter';
|
||||||
|
|
||||||
export function useReturnOrderFields({
|
export function useReturnOrderFields({
|
||||||
@ -22,6 +23,8 @@ export function useReturnOrderFields({
|
|||||||
}: {
|
}: {
|
||||||
duplicateOrderId?: number;
|
duplicateOrderId?: number;
|
||||||
}): ApiFormFieldSet {
|
}): ApiFormFieldSet {
|
||||||
|
const globalSettings = useGlobalSettingsState();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const fields: ApiFormFieldSet = {
|
const fields: ApiFormFieldSet = {
|
||||||
reference: {},
|
reference: {},
|
||||||
@ -82,8 +85,12 @@ export function useReturnOrderFields({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!globalSettings.isSet('PROJECT_CODES_ENABLED', true)) {
|
||||||
|
delete fields.project_code;
|
||||||
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}, [duplicateOrderId]);
|
}, [duplicateOrderId, globalSettings]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useReturnOrderLineItemFields({
|
export function useReturnOrderLineItemFields({
|
||||||
|
@ -16,6 +16,7 @@ import { ApiEndpoints } from '../enums/ApiEndpoints';
|
|||||||
import { ModelType } from '../enums/ModelType';
|
import { ModelType } from '../enums/ModelType';
|
||||||
import { useCreateApiFormModal } from '../hooks/UseForm';
|
import { useCreateApiFormModal } from '../hooks/UseForm';
|
||||||
import { apiUrl } from '../states/ApiState';
|
import { apiUrl } from '../states/ApiState';
|
||||||
|
import { useGlobalSettingsState } from '../states/SettingsState';
|
||||||
import { PartColumn } from '../tables/ColumnRenderers';
|
import { PartColumn } from '../tables/ColumnRenderers';
|
||||||
|
|
||||||
export function useSalesOrderFields({
|
export function useSalesOrderFields({
|
||||||
@ -23,6 +24,8 @@ export function useSalesOrderFields({
|
|||||||
}: {
|
}: {
|
||||||
duplicateOrderId?: number;
|
duplicateOrderId?: number;
|
||||||
}): ApiFormFieldSet {
|
}): ApiFormFieldSet {
|
||||||
|
const globalSettings = useGlobalSettingsState();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const fields: ApiFormFieldSet = {
|
const fields: ApiFormFieldSet = {
|
||||||
reference: {},
|
reference: {},
|
||||||
@ -76,8 +79,12 @@ export function useSalesOrderFields({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!globalSettings.isSet('PROJECT_CODES_ENABLED', true)) {
|
||||||
|
delete fields.project_code;
|
||||||
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}, [duplicateOrderId]);
|
}, [duplicateOrderId, globalSettings]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSalesOrderLineItemFields({
|
export function useSalesOrderLineItemFields({
|
||||||
|
@ -139,12 +139,6 @@ export function constructField({
|
|||||||
};
|
};
|
||||||
|
|
||||||
switch (def.field_type) {
|
switch (def.field_type) {
|
||||||
case 'date':
|
|
||||||
// Change value to a date object if required
|
|
||||||
if (def.value) {
|
|
||||||
def.value = new Date(def.value);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'nested object':
|
case 'nested object':
|
||||||
def.children = {};
|
def.children = {};
|
||||||
for (const k of Object.keys(field.children ?? {})) {
|
for (const k of Object.keys(field.children ?? {})) {
|
||||||
|
@ -26,6 +26,7 @@ import PageTitle from '../../../../components/nav/PageTitle';
|
|||||||
import { SettingsHeader } from '../../../../components/nav/SettingsHeader';
|
import { SettingsHeader } from '../../../../components/nav/SettingsHeader';
|
||||||
import type { PanelType } from '../../../../components/panels/Panel';
|
import type { PanelType } from '../../../../components/panels/Panel';
|
||||||
import { PanelGroup } from '../../../../components/panels/PanelGroup';
|
import { PanelGroup } from '../../../../components/panels/PanelGroup';
|
||||||
|
import { GlobalSettingList } from '../../../../components/settings/SettingList';
|
||||||
import { Loadable } from '../../../../functions/loading';
|
import { Loadable } from '../../../../functions/loading';
|
||||||
import { useUserState } from '../../../../states/UserState';
|
import { useUserState } from '../../../../states/UserState';
|
||||||
|
|
||||||
@ -104,7 +105,7 @@ export default function AdminCenter() {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: 'user',
|
name: 'user',
|
||||||
label: t`Users`,
|
label: t`User Management`,
|
||||||
icon: <IconUsersGroup />,
|
icon: <IconUsersGroup />,
|
||||||
content: <UserManagementPanel />
|
content: <UserManagementPanel />
|
||||||
},
|
},
|
||||||
@ -144,6 +145,7 @@ export default function AdminCenter() {
|
|||||||
icon: <IconListDetails />,
|
icon: <IconListDetails />,
|
||||||
content: (
|
content: (
|
||||||
<Stack gap='xs'>
|
<Stack gap='xs'>
|
||||||
|
<GlobalSettingList keys={['PROJECT_CODES_ENABLED']} />
|
||||||
<ProjectCodeTable />
|
<ProjectCodeTable />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
|
@ -1,46 +1,46 @@
|
|||||||
import { Trans } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Anchor, Divider, Group, Stack, Text, Title } from '@mantine/core';
|
import { Accordion } from '@mantine/core';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
|
import { StylishText } from '../../../../components/items/StylishText';
|
||||||
import { GlobalSettingList } from '../../../../components/settings/SettingList';
|
import { GlobalSettingList } from '../../../../components/settings/SettingList';
|
||||||
import { GroupTable } from '../../../../tables/settings/GroupTable';
|
import { GroupTable } from '../../../../tables/settings/GroupTable';
|
||||||
import { UserTable } from '../../../../tables/settings/UserTable';
|
import { UserTable } from '../../../../tables/settings/UserTable';
|
||||||
|
|
||||||
export default function UserManagementPanel() {
|
export default function UserManagementPanel() {
|
||||||
return (
|
return (
|
||||||
<Stack gap='xs'>
|
<>
|
||||||
<Title order={5}>
|
<Accordion multiple defaultValue={['users']}>
|
||||||
<Trans>Users</Trans>
|
<Accordion.Item value='users' key='users'>
|
||||||
</Title>
|
<Accordion.Control>
|
||||||
|
<StylishText size='lg'>{t`Users`}</StylishText>
|
||||||
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
<UserTable />
|
<UserTable />
|
||||||
|
</Accordion.Panel>
|
||||||
<Divider />
|
</Accordion.Item>
|
||||||
|
<Accordion.Item value='groups' key='groups'>
|
||||||
<Title order={5}>
|
<Accordion.Control>
|
||||||
<Trans>Groups</Trans>
|
<StylishText size='lg'>{t`Groups`}</StylishText>
|
||||||
</Title>
|
</Accordion.Control>
|
||||||
|
<Accordion.Panel>
|
||||||
<GroupTable />
|
<GroupTable />
|
||||||
|
</Accordion.Panel>
|
||||||
<Divider />
|
</Accordion.Item>
|
||||||
|
<Accordion.Item value='settings' key='settings'>
|
||||||
<Stack gap={0}>
|
<Accordion.Control>
|
||||||
<Text>
|
<StylishText size='lg'>{t`Settings`}</StylishText>
|
||||||
<Trans>Settings</Trans>
|
</Accordion.Control>
|
||||||
</Text>
|
<Accordion.Panel>
|
||||||
<Group>
|
|
||||||
<Text c='dimmed'>
|
|
||||||
<Trans>
|
|
||||||
Select settings relevant for user lifecycle. More available in
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
<Anchor component={Link} to={'/settings/system'}>
|
|
||||||
<Trans>System Settings</Trans>
|
|
||||||
</Anchor>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
<GlobalSettingList
|
<GlobalSettingList
|
||||||
keys={['LOGIN_ENABLE_REG', 'SIGNUP_GROUP', 'LOGIN_ENABLE_SSO_REG']}
|
keys={[
|
||||||
|
'LOGIN_ENABLE_REG',
|
||||||
|
'SIGNUP_GROUP',
|
||||||
|
'LOGIN_ENABLE_SSO_REG'
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
</Accordion>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -181,21 +181,28 @@ export default function BuildDetail() {
|
|||||||
hidden: !build.responsible
|
hidden: !build.responsible
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'date',
|
||||||
name: 'creation_date',
|
name: 'creation_date',
|
||||||
label: t`Created`,
|
label: t`Created`,
|
||||||
icon: 'calendar',
|
icon: 'calendar',
|
||||||
hidden: !build.creation_date
|
hidden: !build.creation_date
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'date',
|
||||||
|
name: 'start_date',
|
||||||
|
label: t`Start Date`,
|
||||||
|
icon: 'calendar',
|
||||||
|
hidden: !build.start_date
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'date',
|
||||||
name: 'target_date',
|
name: 'target_date',
|
||||||
label: t`Target Date`,
|
label: t`Target Date`,
|
||||||
icon: 'calendar',
|
icon: 'calendar',
|
||||||
hidden: !build.target_date
|
hidden: !build.target_date
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'date',
|
||||||
name: 'completion_date',
|
name: 'completion_date',
|
||||||
label: t`Completed`,
|
label: t`Completed`,
|
||||||
icon: 'calendar',
|
icon: 'calendar',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Grid, Skeleton, Stack } from '@mantine/core';
|
import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||||
import { IconInfoCircle, IconPackages } from '@tabler/icons-react';
|
import { IconBookmark, IconInfoCircle } from '@tabler/icons-react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
@ -208,8 +208,8 @@ export default function SalesOrderShipmentDetail() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'items',
|
name: 'items',
|
||||||
label: t`Assigned Items`,
|
label: t`Allocated Stock`,
|
||||||
icon: <IconPackages />,
|
icon: <IconBookmark />,
|
||||||
content: (
|
content: (
|
||||||
<SalesOrderAllocationTable
|
<SalesOrderAllocationTable
|
||||||
shipmentId={shipment.pk}
|
shipmentId={shipment.pk}
|
||||||
|
@ -667,13 +667,15 @@ export default function StockDetail() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const stockActions = useMemo(() => {
|
const stockActions = useMemo(() => {
|
||||||
const inStock =
|
// Can this stock item be transferred to a different location?
|
||||||
|
const canTransfer =
|
||||||
user.hasChangeRole(UserRoles.stock) &&
|
user.hasChangeRole(UserRoles.stock) &&
|
||||||
!stockitem.sales_order &&
|
!stockitem.sales_order &&
|
||||||
!stockitem.belongs_to &&
|
!stockitem.belongs_to &&
|
||||||
!stockitem.customer &&
|
!stockitem.customer &&
|
||||||
!stockitem.consumed_by &&
|
!stockitem.consumed_by;
|
||||||
!stockitem.is_building;
|
|
||||||
|
const isBuilding = stockitem.is_building;
|
||||||
|
|
||||||
const serial = stockitem.serial;
|
const serial = stockitem.serial;
|
||||||
const serialized =
|
const serialized =
|
||||||
@ -704,7 +706,7 @@ export default function StockDetail() {
|
|||||||
{
|
{
|
||||||
name: t`Count`,
|
name: t`Count`,
|
||||||
tooltip: t`Count stock`,
|
tooltip: t`Count stock`,
|
||||||
hidden: serialized || !inStock,
|
hidden: serialized || !canTransfer || isBuilding,
|
||||||
icon: (
|
icon: (
|
||||||
<InvenTreeIcon icon='stocktake' iconProps={{ color: 'blue' }} />
|
<InvenTreeIcon icon='stocktake' iconProps={{ color: 'blue' }} />
|
||||||
),
|
),
|
||||||
@ -715,7 +717,7 @@ export default function StockDetail() {
|
|||||||
{
|
{
|
||||||
name: t`Add`,
|
name: t`Add`,
|
||||||
tooltip: t`Add Stock`,
|
tooltip: t`Add Stock`,
|
||||||
hidden: serialized || !inStock,
|
hidden: serialized || !canTransfer || isBuilding,
|
||||||
icon: <InvenTreeIcon icon='add' iconProps={{ color: 'green' }} />,
|
icon: <InvenTreeIcon icon='add' iconProps={{ color: 'green' }} />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
stockitem.pk && addStockItem.open();
|
stockitem.pk && addStockItem.open();
|
||||||
@ -724,7 +726,11 @@ export default function StockDetail() {
|
|||||||
{
|
{
|
||||||
name: t`Remove`,
|
name: t`Remove`,
|
||||||
tooltip: t`Remove Stock`,
|
tooltip: t`Remove Stock`,
|
||||||
hidden: serialized || !inStock || stockitem.quantity <= 0,
|
hidden:
|
||||||
|
serialized ||
|
||||||
|
!canTransfer ||
|
||||||
|
isBuilding ||
|
||||||
|
stockitem.quantity <= 0,
|
||||||
icon: <InvenTreeIcon icon='remove' iconProps={{ color: 'red' }} />,
|
icon: <InvenTreeIcon icon='remove' iconProps={{ color: 'red' }} />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
stockitem.pk && removeStockItem.open();
|
stockitem.pk && removeStockItem.open();
|
||||||
@ -733,7 +739,7 @@ export default function StockDetail() {
|
|||||||
{
|
{
|
||||||
name: t`Transfer`,
|
name: t`Transfer`,
|
||||||
tooltip: t`Transfer Stock`,
|
tooltip: t`Transfer Stock`,
|
||||||
hidden: !inStock,
|
hidden: !canTransfer,
|
||||||
icon: (
|
icon: (
|
||||||
<InvenTreeIcon icon='transfer' iconProps={{ color: 'blue' }} />
|
<InvenTreeIcon icon='transfer' iconProps={{ color: 'blue' }} />
|
||||||
),
|
),
|
||||||
@ -745,8 +751,10 @@ export default function StockDetail() {
|
|||||||
name: t`Serialize`,
|
name: t`Serialize`,
|
||||||
tooltip: t`Serialize stock`,
|
tooltip: t`Serialize stock`,
|
||||||
hidden:
|
hidden:
|
||||||
!inStock ||
|
!canTransfer ||
|
||||||
|
isBuilding ||
|
||||||
serialized ||
|
serialized ||
|
||||||
|
stockitem?.quantity != 1 ||
|
||||||
stockitem?.part_detail?.trackable != true,
|
stockitem?.part_detail?.trackable != true,
|
||||||
icon: <InvenTreeIcon icon='serial' iconProps={{ color: 'blue' }} />,
|
icon: <InvenTreeIcon icon='serial' iconProps={{ color: 'blue' }} />,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
@ -14,6 +14,7 @@ import { formatCurrency, formatDate } from '../defaults/formatters';
|
|||||||
import type { ModelType } from '../enums/ModelType';
|
import type { ModelType } from '../enums/ModelType';
|
||||||
import { resolveItem } from '../functions/conversion';
|
import { resolveItem } from '../functions/conversion';
|
||||||
import { cancelEvent } from '../functions/events';
|
import { cancelEvent } from '../functions/events';
|
||||||
|
import { useGlobalSettingsState } from '../states/SettingsState';
|
||||||
import type { TableColumn, TableColumnProps } from './Column';
|
import type { TableColumn, TableColumnProps } from './Column';
|
||||||
import { ProjectCodeHoverCard } from './TableHoverCard';
|
import { ProjectCodeHoverCard } from './TableHoverCard';
|
||||||
|
|
||||||
@ -161,11 +162,15 @@ export function LineItemsProgressColumn(): TableColumn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ProjectCodeColumn(props: TableColumnProps): TableColumn {
|
export function ProjectCodeColumn(props: TableColumnProps): TableColumn {
|
||||||
|
const globalSettings = useGlobalSettingsState.getState();
|
||||||
|
const enabled = globalSettings.isSet('PROJECT_CODES_ENABLED', true);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessor: 'project_code',
|
accessor: 'project_code',
|
||||||
ordering: 'project_code',
|
ordering: 'project_code',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
title: t`Project Code`,
|
title: t`Project Code`,
|
||||||
|
hidden: !enabled,
|
||||||
render: (record: any) => {
|
render: (record: any) => {
|
||||||
const project_code = resolveItem(
|
const project_code = resolveItem(
|
||||||
record,
|
record,
|
||||||
|
@ -5,6 +5,7 @@ import type {
|
|||||||
StatusCodeListInterface
|
StatusCodeListInterface
|
||||||
} from '../components/render/StatusRenderer';
|
} from '../components/render/StatusRenderer';
|
||||||
import type { ModelType } from '../enums/ModelType';
|
import type { ModelType } from '../enums/ModelType';
|
||||||
|
import { useGlobalSettingsState } from '../states/SettingsState';
|
||||||
import { type StatusLookup, useGlobalStatusState } from '../states/StatusState';
|
import { type StatusLookup, useGlobalStatusState } from '../states/StatusState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,9 +29,16 @@ export type TableFilterType = 'boolean' | 'choice' | 'date' | 'text';
|
|||||||
/**
|
/**
|
||||||
* Interface for the table filter type. Provides a number of options for selecting filter value:
|
* Interface for the table filter type. Provides a number of options for selecting filter value:
|
||||||
*
|
*
|
||||||
|
* name: The name of the filter (used for query string)
|
||||||
|
* label: The label to display in the UI (human readable)
|
||||||
|
* description: A description of the filter (human readable)
|
||||||
|
* type: The type of filter (see TableFilterType)
|
||||||
* choices: A list of TableFilterChoice objects
|
* choices: A list of TableFilterChoice objects
|
||||||
* choiceFunction: A function which returns a list of TableFilterChoice objects
|
* choiceFunction: A function which returns a list of TableFilterChoice objects
|
||||||
* statusType: A ModelType which is used to generate a list of status codes
|
* defaultValue: The default value for the filter
|
||||||
|
* value: The current value of the filter
|
||||||
|
* displayValue: The current display value of the filter
|
||||||
|
* active: Whether the filter is active (false = hidden, not used)
|
||||||
*/
|
*/
|
||||||
export type TableFilter = {
|
export type TableFilter = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -198,11 +206,15 @@ export function CompletedAfterFilter(): TableFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function HasProjectCodeFilter(): TableFilter {
|
export function HasProjectCodeFilter(): TableFilter {
|
||||||
|
const globalSettings = useGlobalSettingsState.getState();
|
||||||
|
const enabled = globalSettings.isSet('PROJECT_CODES_ENABLED', true);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'has_project_code',
|
name: 'has_project_code',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: t`Has Project Code`,
|
label: t`Has Project Code`,
|
||||||
description: t`Show orders with an assigned project code`
|
description: t`Show orders with an assigned project code`,
|
||||||
|
active: enabled
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,10 +232,14 @@ export function OrderStatusFilter({
|
|||||||
export function ProjectCodeFilter({
|
export function ProjectCodeFilter({
|
||||||
choices
|
choices
|
||||||
}: { choices: TableFilterChoice[] }): TableFilter {
|
}: { choices: TableFilterChoice[] }): TableFilter {
|
||||||
|
const globalSettings = useGlobalSettingsState.getState();
|
||||||
|
const enabled = globalSettings.isSet('PROJECT_CODES_ENABLED', true);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'project_code',
|
name: 'project_code',
|
||||||
label: t`Project Code`,
|
label: t`Project Code`,
|
||||||
description: t`Filter by project code`,
|
description: t`Filter by project code`,
|
||||||
|
active: enabled,
|
||||||
choices: choices
|
choices: choices
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -105,6 +105,11 @@ export function BuildOrderTable({
|
|||||||
sortable: true
|
sortable: true
|
||||||
},
|
},
|
||||||
CreationDateColumn({}),
|
CreationDateColumn({}),
|
||||||
|
DateColumn({
|
||||||
|
accessor: 'start_date',
|
||||||
|
title: t`Start Date`,
|
||||||
|
sortable: true
|
||||||
|
}),
|
||||||
TargetDateColumn({}),
|
TargetDateColumn({}),
|
||||||
DateColumn({
|
DateColumn({
|
||||||
accessor: 'completion_date',
|
accessor: 'completion_date',
|
||||||
@ -138,6 +143,30 @@ export function BuildOrderTable({
|
|||||||
CreatedAfterFilter(),
|
CreatedAfterFilter(),
|
||||||
TargetDateBeforeFilter(),
|
TargetDateBeforeFilter(),
|
||||||
TargetDateAfterFilter(),
|
TargetDateAfterFilter(),
|
||||||
|
{
|
||||||
|
name: 'start_date_before',
|
||||||
|
type: 'date',
|
||||||
|
label: t`Start Date Before`,
|
||||||
|
description: t`Show items with a start date before this date`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'start_date_after',
|
||||||
|
type: 'date',
|
||||||
|
label: t`Start Date After`,
|
||||||
|
description: t`Show items with a start date after this date`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'has_target_date',
|
||||||
|
type: 'boolean',
|
||||||
|
label: t`Has Target Date`,
|
||||||
|
description: t`Show orders with a target date`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'has_start_date',
|
||||||
|
type: 'boolean',
|
||||||
|
label: t`Has Start Date`,
|
||||||
|
description: t`Show orders with a start date`
|
||||||
|
},
|
||||||
CompletedBeforeFilter(),
|
CompletedBeforeFilter(),
|
||||||
CompletedAfterFilter(),
|
CompletedAfterFilter(),
|
||||||
ProjectCodeFilter({ choices: projectCodeFilters.choices }),
|
ProjectCodeFilter({ choices: projectCodeFilters.choices }),
|
||||||
|
@ -575,6 +575,7 @@ export default function BuildOutputTable({
|
|||||||
props={{
|
props={{
|
||||||
params: {
|
params: {
|
||||||
part_detail: true,
|
part_detail: true,
|
||||||
|
location_detail: true,
|
||||||
tests: true,
|
tests: true,
|
||||||
is_building: true,
|
is_building: true,
|
||||||
build: buildId
|
build: buildId
|
||||||
|
@ -120,6 +120,18 @@ export default function SalesOrderAllocationTable({
|
|||||||
switchable: false,
|
switchable: false,
|
||||||
render: (record: any) => PartColumn({ part: record.part_detail })
|
render: (record: any) => PartColumn({ part: record.part_detail })
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessor: 'part_detail.description',
|
||||||
|
title: t`Description`,
|
||||||
|
hidden: showPartInfo != true,
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'part_detail.IPN',
|
||||||
|
title: t`IPN`,
|
||||||
|
hidden: showPartInfo != true,
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessor: 'serial',
|
accessor: 'serial',
|
||||||
title: t`Serial Number`,
|
title: t`Serial Number`,
|
||||||
|
@ -43,7 +43,11 @@ export const setTableChoiceFilter = async (page, filter, value) => {
|
|||||||
await page.getByRole('button', { name: 'Add Filter' }).click();
|
await page.getByRole('button', { name: 'Add Filter' }).click();
|
||||||
await page.getByPlaceholder('Select filter').fill(filter);
|
await page.getByPlaceholder('Select filter').fill(filter);
|
||||||
await page.getByPlaceholder('Select filter').click();
|
await page.getByPlaceholder('Select filter').click();
|
||||||
await page.getByRole('option', { name: filter }).click();
|
|
||||||
|
// Construct a regex to match the filter name exactly
|
||||||
|
const filterRegex = new RegExp(`^${filter}$`, 'i');
|
||||||
|
|
||||||
|
await page.getByRole('option', { name: filterRegex }).click();
|
||||||
|
|
||||||
await page.getByPlaceholder('Select filter value').click();
|
await page.getByPlaceholder('Select filter value').click();
|
||||||
await page.getByRole('option', { name: value }).click();
|
await page.getByRole('option', { name: value }).click();
|
||||||
|
@ -87,6 +87,33 @@ test('Build Order - Basic Tests', async ({ page }) => {
|
|||||||
.waitFor();
|
.waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Build Order - Edit', async ({ page }) => {
|
||||||
|
await doQuickLogin(page);
|
||||||
|
|
||||||
|
await page.goto(`${baseUrl}/manufacturing/build-order/22/`);
|
||||||
|
|
||||||
|
// Check for expected text items
|
||||||
|
await page.getByText('Building for sales order').first().waitFor();
|
||||||
|
await page.getByText('2024-08-08').waitFor(); // Created date
|
||||||
|
await page.getByText('2025-01-01').waitFor(); // Start date
|
||||||
|
await page.getByText('2025-01-22').waitFor(); // Target date
|
||||||
|
|
||||||
|
await page.keyboard.press('Control+E');
|
||||||
|
|
||||||
|
// Edit start date
|
||||||
|
await page.getByLabel('date-field-start_date').fill('2026-09-09');
|
||||||
|
|
||||||
|
// Submit the form
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
// Expect error
|
||||||
|
await page.getByText('Errors exist for one or more form fields').waitFor();
|
||||||
|
await page.getByText('Target date must be after start date').waitFor();
|
||||||
|
|
||||||
|
// Cancel the form
|
||||||
|
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
test('Build Order - Build Outputs', async ({ page }) => {
|
test('Build Order - Build Outputs', async ({ page }) => {
|
||||||
await doQuickLogin(page);
|
await doQuickLogin(page);
|
||||||
|
|
||||||
|
@ -155,7 +155,7 @@ test('Sales Orders - Shipments', async ({ page }) => {
|
|||||||
// Click through the various tabs
|
// Click through the various tabs
|
||||||
await page.getByRole('tab', { name: 'Attachments' }).click();
|
await page.getByRole('tab', { name: 'Attachments' }).click();
|
||||||
await page.getByRole('tab', { name: 'Notes' }).click();
|
await page.getByRole('tab', { name: 'Notes' }).click();
|
||||||
await page.getByRole('tab', { name: 'Assigned Items' }).click();
|
await page.getByRole('tab', { name: 'Allocated Stock' }).click();
|
||||||
|
|
||||||
// Ensure assigned items table loads correctly
|
// Ensure assigned items table loads correctly
|
||||||
await page.getByRole('cell', { name: 'BATCH-001' }).first().waitFor();
|
await page.getByRole('cell', { name: 'BATCH-001' }).first().waitFor();
|
||||||
|
@ -1,51 +1,38 @@
|
|||||||
import { test } from './baseFixtures.js';
|
import { test } from './baseFixtures.js';
|
||||||
import { baseUrl } from './defaults.js';
|
import { baseUrl } from './defaults.js';
|
||||||
import {
|
import { clearTableFilters, setTableChoiceFilter } from './helpers.js';
|
||||||
clearTableFilters,
|
|
||||||
closeFilterDrawer,
|
|
||||||
openFilterDrawer
|
|
||||||
} from './helpers.js';
|
|
||||||
import { doQuickLogin } from './login.js';
|
import { doQuickLogin } from './login.js';
|
||||||
|
|
||||||
// Helper function to set the value of a specific table filter
|
|
||||||
const setFilter = async (page, name: string, value: string) => {
|
|
||||||
await openFilterDrawer(page);
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Add Filter' }).click();
|
|
||||||
await page.getByPlaceholder('Select filter').click();
|
|
||||||
await page.getByRole('option', { name: name, exact: true }).click();
|
|
||||||
await page.getByPlaceholder('Select filter value').click();
|
|
||||||
await page.getByRole('option', { name: value, exact: true }).click();
|
|
||||||
|
|
||||||
await closeFilterDrawer(page);
|
|
||||||
};
|
|
||||||
|
|
||||||
test('Tables - Filters', async ({ page }) => {
|
test('Tables - Filters', async ({ page }) => {
|
||||||
await doQuickLogin(page);
|
await doQuickLogin(page);
|
||||||
|
|
||||||
// Head to the "build order list" page
|
// Head to the "build order list" page
|
||||||
await page.goto(`${baseUrl}/manufacturing/index/`);
|
await page.goto(`${baseUrl}/manufacturing/index/`);
|
||||||
|
|
||||||
await setFilter(page, 'Status', 'Complete');
|
await clearTableFilters(page);
|
||||||
await setFilter(page, 'Responsible', 'allaccess');
|
|
||||||
await setFilter(page, 'Project Code', 'PRJ-NIM');
|
await setTableChoiceFilter(page, 'Status', 'Complete');
|
||||||
|
await setTableChoiceFilter(page, 'Responsible', 'allaccess');
|
||||||
|
await setTableChoiceFilter(page, 'Project Code', 'PRJ-NIM');
|
||||||
|
|
||||||
await clearTableFilters(page);
|
await clearTableFilters(page);
|
||||||
|
|
||||||
// Head to the "part list" page
|
// Head to the "part list" page
|
||||||
await page.goto(`${baseUrl}/part/category/index/parts/`);
|
await page.goto(`${baseUrl}/part/category/index/parts/`);
|
||||||
|
|
||||||
await setFilter(page, 'Assembly', 'Yes');
|
await setTableChoiceFilter(page, 'Assembly', 'Yes');
|
||||||
|
|
||||||
await clearTableFilters(page);
|
await clearTableFilters(page);
|
||||||
|
|
||||||
// Head to the "purchase order list" page
|
// Head to the "purchase order list" page
|
||||||
await page.goto(`${baseUrl}/purchasing/index/purchaseorders/`);
|
await page.goto(`${baseUrl}/purchasing/index/purchaseorders/`);
|
||||||
|
|
||||||
await setFilter(page, 'Status', 'Complete');
|
await clearTableFilters(page);
|
||||||
await setFilter(page, 'Responsible', 'readers');
|
|
||||||
await setFilter(page, 'Assigned to me', 'No');
|
await setTableChoiceFilter(page, 'Status', 'Complete');
|
||||||
await setFilter(page, 'Project Code', 'PRO-ZEN');
|
await setTableChoiceFilter(page, 'Responsible', 'readers');
|
||||||
|
await setTableChoiceFilter(page, 'Assigned to me', 'No');
|
||||||
|
await setTableChoiceFilter(page, 'Project Code', 'PRO-ZEN');
|
||||||
|
|
||||||
await clearTableFilters(page);
|
await clearTableFilters(page);
|
||||||
});
|
});
|
||||||
|
File diff suppressed because it is too large
Load Diff
9
tasks.py
9
tasks.py
@ -418,7 +418,7 @@ def remove_mfa(c, mail=''):
|
|||||||
def static(c, frontend=False, clear=True, skip_plugins=False):
|
def static(c, frontend=False, clear=True, skip_plugins=False):
|
||||||
"""Copies required static files to the STATIC_ROOT directory, as per Django requirements."""
|
"""Copies required static files to the STATIC_ROOT directory, as per Django requirements."""
|
||||||
if frontend and node_available():
|
if frontend and node_available():
|
||||||
frontend_trans(c)
|
frontend_trans(c, extract=False)
|
||||||
frontend_build(c)
|
frontend_build(c)
|
||||||
|
|
||||||
info('Collecting static files...')
|
info('Collecting static files...')
|
||||||
@ -1227,7 +1227,7 @@ def frontend_compile(c):
|
|||||||
"""
|
"""
|
||||||
info('Compiling frontend code...')
|
info('Compiling frontend code...')
|
||||||
frontend_install(c)
|
frontend_install(c)
|
||||||
frontend_trans(c)
|
frontend_trans(c, extract=False)
|
||||||
frontend_build(c)
|
frontend_build(c)
|
||||||
success('Frontend compilation complete')
|
success('Frontend compilation complete')
|
||||||
|
|
||||||
@ -1243,14 +1243,15 @@ def frontend_install(c):
|
|||||||
yarn(c, 'yarn install')
|
yarn(c, 'yarn install')
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task(help={'extract': 'Extract translations (changes sourcecode), default: True'})
|
||||||
def frontend_trans(c):
|
def frontend_trans(c, extract: bool = True):
|
||||||
"""Compile frontend translations.
|
"""Compile frontend translations.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
c: Context variable
|
c: Context variable
|
||||||
"""
|
"""
|
||||||
info('Compiling frontend translations')
|
info('Compiling frontend translations')
|
||||||
|
if extract:
|
||||||
yarn(c, 'yarn run extract')
|
yarn(c, 'yarn run extract')
|
||||||
yarn(c, 'yarn run compile')
|
yarn(c, 'yarn run compile')
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user