mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Batch code generation (#7000)
* Refactor framework for generating batch codes - Provide additional kwargs to plugin - Move into new file - Error handling * Implement API endpoint for generating a new batch code * Fixes * Refactor into stock.generators * Fix API endpoint * Pass time context through to plugins * Generate batch code when receiving items * Create useGenerator hook - Build up a dataset and query server whenever it changes - Look for result in response data - For now, just used for generating batch codes - may be used for more in the future * Refactor PurchaseOrderForms to use new generator hook * Refactor StockForms implementation * Remove dead code * add OAS diff * fix ref * fix ref again * wrong branch, sorry * Update src/frontend/src/hooks/UseGenerator.tsx Co-authored-by: Lukas <76838159+wolflu05@users.noreply.github.com> * Bump API version * Do not override batch code if already generated * Add serial number generator - Move to /generate/ API endpoint - Move batch code generator too * Update PUI endpoints * Add debouncing to useGenerator hook * Refactor useGenerator func * Add serial number generator to stock form * Add batch code genereator to build order form * Update buildfields * Use build batch code when creating new output --------- Co-authored-by: Matthias Mair <code@mjmair.com> Co-authored-by: Lukas <76838159+wolflu05@users.noreply.github.com>
This commit is contained in:
parent
5cb61d5ad0
commit
e93d9c4a74
@ -116,7 +116,7 @@ Validation of the Part IPN (Internal Part Number) field is exposed to custom plu
|
||||
|
||||
The `validate_batch_code` method allows plugins to raise an error if a batch code input by the user does not meet a particular pattern.
|
||||
|
||||
The `generate_batch_code` method can be implemented to generate a new batch code.
|
||||
The `generate_batch_code` method can be implemented to generate a new batch code, based on a set of provided information.
|
||||
|
||||
### Serial Numbers
|
||||
|
||||
|
@ -8,6 +8,7 @@ from pathlib import Path
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.http import JsonResponse
|
||||
from django.urls import include, path
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from django_q.models import OrmQ
|
||||
|
@ -1,11 +1,16 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 199
|
||||
INVENTREE_API_VERSION = 200
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v200 - 2024-05-20 : https://github.com/inventree/InvenTree/pull/7000
|
||||
- Adds API endpoint for generating custom batch codes
|
||||
- Adds API endpoint for generating custom serial numbers
|
||||
|
||||
v199 - 2024-05-20 : https://github.com/inventree/InvenTree/pull/7264
|
||||
- Expose "bom_valid" filter for the Part API
|
||||
- Expose "starred" filter for the Part API
|
||||
|
@ -293,13 +293,13 @@ def increment(value):
|
||||
QQQ -> QQQ
|
||||
|
||||
"""
|
||||
value = str(value).strip()
|
||||
|
||||
# Ignore empty strings
|
||||
if value in ['', None]:
|
||||
# Provide a default value if provided with a null input
|
||||
return '1'
|
||||
|
||||
value = str(value).strip()
|
||||
|
||||
pattern = r'(.*?)(\d+)?$'
|
||||
|
||||
result = re.search(pattern, value)
|
||||
|
@ -86,6 +86,21 @@ apipatterns = [
|
||||
path('part/', include(part.api.part_api_urls)),
|
||||
path('bom/', include(part.api.bom_api_urls)),
|
||||
path('company/', include(company.api.company_api_urls)),
|
||||
path(
|
||||
'generate/',
|
||||
include([
|
||||
path(
|
||||
'batch-code/',
|
||||
stock.api.GenerateBatchCode.as_view(),
|
||||
name='api-generate-batch-code',
|
||||
),
|
||||
path(
|
||||
'serial-number/',
|
||||
stock.api.GenerateSerialNumber.as_view(),
|
||||
name='api-generate-serial-number',
|
||||
),
|
||||
]),
|
||||
),
|
||||
path('stock/', include(stock.api.stock_api_urls)),
|
||||
path('build/', include(build.api.build_api_urls)),
|
||||
path('order/', include(order.api.order_api_urls)),
|
||||
|
@ -1,7 +1,5 @@
|
||||
"""JSON serializers for Build API."""
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import transaction
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -22,7 +20,8 @@ import InvenTree.helpers
|
||||
from InvenTree.serializers import InvenTreeDecimalField
|
||||
from InvenTree.status_codes import StockStatus
|
||||
|
||||
from stock.models import generate_batch_code, StockItem, StockLocation
|
||||
from stock.generators import generate_batch_code
|
||||
from stock.models import StockItem, StockLocation
|
||||
from stock.serializers import StockItemSerializerBrief, LocationSerializer
|
||||
|
||||
import common.models
|
||||
|
@ -129,9 +129,14 @@ class ValidationMixin:
|
||||
"""
|
||||
return None
|
||||
|
||||
def generate_batch_code(self):
|
||||
def generate_batch_code(self, **kwargs):
|
||||
"""Generate a new batch code.
|
||||
|
||||
This method is called when a new batch code is required.
|
||||
|
||||
kwargs:
|
||||
Any additional keyword arguments which are passed through to the plugin, based on the context of the caller
|
||||
|
||||
Returns:
|
||||
A new batch code (string) or None
|
||||
"""
|
||||
|
@ -145,7 +145,17 @@ class SampleValidatorPlugin(SettingsMixin, ValidationMixin, InvenTreePlugin):
|
||||
if len(batch_code) > 0 and prefix and not batch_code.startswith(prefix):
|
||||
self.raise_error(f"Batch code must start with '{prefix}'")
|
||||
|
||||
def generate_batch_code(self):
|
||||
def generate_batch_code(self, **kwargs):
|
||||
"""Generate a new batch code."""
|
||||
now = datetime.now()
|
||||
return f'BATCH-{now.year}:{now.month}:{now.day}'
|
||||
batch = f'SAMPLE-BATCH-{now.year}:{now.month}:{now.day}'
|
||||
|
||||
# If a Part instance is provided, prepend the part name to the batch code
|
||||
if part := kwargs.get('part', None):
|
||||
batch = f'{part.name}-{batch}'
|
||||
|
||||
# If a Build instance is provided, prepend the build number to the batch code
|
||||
if build := kwargs.get('build_order', None):
|
||||
batch = f'{build.reference}-{batch}'
|
||||
|
||||
return batch
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""JSON API for the Stock app."""
|
||||
|
||||
import json
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
|
||||
@ -13,7 +14,8 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django_filters import rest_framework as rest_filters
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import status
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
@ -64,6 +66,7 @@ from order.serializers import (
|
||||
from part.models import BomItem, Part, PartCategory
|
||||
from part.serializers import PartBriefSerializer
|
||||
from stock.admin import LocationResource, StockItemResource
|
||||
from stock.generators import generate_batch_code, generate_serial_number
|
||||
from stock.models import (
|
||||
StockItem,
|
||||
StockItemAttachment,
|
||||
@ -74,6 +77,38 @@ from stock.models import (
|
||||
)
|
||||
|
||||
|
||||
class GenerateBatchCode(GenericAPIView):
|
||||
"""API endpoint for generating batch codes."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = StockSerializers.GenerateBatchCodeSerializer
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Generate a new batch code."""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
data = {'batch_code': generate_batch_code(**serializer.validated_data)}
|
||||
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class GenerateSerialNumber(GenericAPIView):
|
||||
"""API endpoint for generating serial numbers."""
|
||||
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = StockSerializers.GenerateSerialNumberSerializer
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Generate a new serial number."""
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
data = {'serial_number': generate_serial_number(**serializer.validated_data)}
|
||||
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class StockDetail(RetrieveUpdateDestroyAPI):
|
||||
"""API detail endpoint for Stock object.
|
||||
|
||||
|
113
src/backend/InvenTree/stock/generators.py
Normal file
113
src/backend/InvenTree/stock/generators.py
Normal file
@ -0,0 +1,113 @@
|
||||
"""Generator functions for the stock app."""
|
||||
|
||||
from inspect import signature
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from jinja2 import Template
|
||||
|
||||
import common.models
|
||||
import InvenTree.exceptions
|
||||
import InvenTree.helpers
|
||||
|
||||
|
||||
def generate_batch_code(**kwargs):
|
||||
"""Generate a default 'batch code' for a new StockItem.
|
||||
|
||||
By default, this uses the value of the 'STOCK_BATCH_CODE_TEMPLATE' setting (if configured),
|
||||
which can be passed through a simple template.
|
||||
|
||||
Also, this function is exposed to the ValidationMixin plugin class,
|
||||
allowing custom plugins to be used to generate new batch code values.
|
||||
|
||||
Various kwargs can be passed to the function, which will be passed through to the plugin functions.
|
||||
"""
|
||||
# First, check if any plugins can generate batch codes
|
||||
from plugin.registry import registry
|
||||
|
||||
now = InvenTree.helpers.current_time()
|
||||
|
||||
context = {
|
||||
'date': now,
|
||||
'year': now.year,
|
||||
'month': now.month,
|
||||
'day': now.day,
|
||||
'hour': now.hour,
|
||||
'minute': now.minute,
|
||||
'week': now.isocalendar()[1],
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
for plugin in registry.with_mixin('validation'):
|
||||
generate = getattr(plugin, 'generate_batch_code', None)
|
||||
|
||||
if not generate:
|
||||
continue
|
||||
|
||||
# Check if the function signature accepts kwargs
|
||||
sig = signature(generate)
|
||||
|
||||
if 'kwargs' in sig.parameters:
|
||||
# Pass the kwargs through to the plugin
|
||||
try:
|
||||
batch = generate(**context)
|
||||
except Exception:
|
||||
InvenTree.exceptions.log_error('plugin.generate_batch_code')
|
||||
continue
|
||||
else:
|
||||
# Ignore the kwargs (legacy plugin)
|
||||
try:
|
||||
batch = generate()
|
||||
except Exception:
|
||||
InvenTree.exceptions.log_error('plugin.generate_batch_code')
|
||||
continue
|
||||
|
||||
# Return the first non-null value generated by a plugin
|
||||
if batch is not None:
|
||||
return batch
|
||||
|
||||
# If we get to this point, no plugin was able to generate a new batch code
|
||||
batch_template = common.models.InvenTreeSetting.get_setting(
|
||||
'STOCK_BATCH_CODE_TEMPLATE', ''
|
||||
)
|
||||
|
||||
return Template(batch_template).render(context)
|
||||
|
||||
|
||||
def generate_serial_number(part=None, quantity=1, **kwargs) -> str:
|
||||
"""Generate a default 'serial number' for a new StockItem."""
|
||||
from plugin.registry import registry
|
||||
|
||||
quantity = quantity or 1
|
||||
|
||||
if part is None:
|
||||
# Cannot generate a serial number without a part
|
||||
return None
|
||||
|
||||
try:
|
||||
quantity = int(quantity)
|
||||
except Exception:
|
||||
raise ValidationError({'quantity': 'Invalid quantity value'})
|
||||
|
||||
if quantity < 1:
|
||||
raise ValidationError({'quantity': 'Quantity must be greater than zero'})
|
||||
|
||||
# If we are here, no plugins were available to generate a serial number
|
||||
# In this case, we will generate a simple serial number based on the provided part
|
||||
sn = part.get_latest_serial_number()
|
||||
|
||||
serials = []
|
||||
|
||||
# Generate the required quantity of serial numbers
|
||||
# Note that this call gets passed through to the plugin system
|
||||
while quantity > 0:
|
||||
sn = InvenTree.helpers.increment_serial_number(sn)
|
||||
|
||||
# Exit if an empty or duplicated serial is generated
|
||||
if not sn or sn in serials:
|
||||
break
|
||||
|
||||
serials.append(sn)
|
||||
quantity -= 1
|
||||
|
||||
return ','.join(serials)
|
@ -1,6 +1,7 @@
|
||||
# Generated by Django 3.2.12 on 2022-04-26 10:19
|
||||
|
||||
from django.db import migrations, models
|
||||
import stock.generators
|
||||
import stock.models
|
||||
|
||||
|
||||
@ -14,6 +15,6 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='stockitem',
|
||||
name='batch',
|
||||
field=models.CharField(blank=True, default=stock.models.generate_batch_code, help_text='Batch code for this stock item', max_length=100, null=True, verbose_name='Batch Code'),
|
||||
field=models.CharField(blank=True, default=stock.generators.generate_batch_code, help_text='Batch code for this stock item', max_length=100, null=True, verbose_name='Batch Code'),
|
||||
),
|
||||
]
|
||||
|
@ -20,7 +20,6 @@ from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from jinja2 import Template
|
||||
from mptt.managers import TreeManager
|
||||
from mptt.models import MPTTModel, TreeForeignKey
|
||||
from taggit.managers import TaggableManager
|
||||
@ -43,6 +42,7 @@ from InvenTree.status_codes import (
|
||||
)
|
||||
from part import models as PartModels
|
||||
from plugin.events import trigger_event
|
||||
from stock.generators import generate_batch_code
|
||||
from users.models import Owner
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
@ -295,47 +295,6 @@ class StockLocation(
|
||||
return self.get_stock_items(cascade=cascade)
|
||||
|
||||
|
||||
def generate_batch_code():
|
||||
"""Generate a default 'batch code' for a new StockItem.
|
||||
|
||||
By default, this uses the value of the 'STOCK_BATCH_CODE_TEMPLATE' setting (if configured),
|
||||
which can be passed through a simple template.
|
||||
|
||||
Also, this function is exposed to the ValidationMixin plugin class,
|
||||
allowing custom plugins to be used to generate new batch code values
|
||||
"""
|
||||
# First, check if any plugins can generate batch codes
|
||||
from plugin.registry import registry
|
||||
|
||||
for plugin in registry.with_mixin('validation'):
|
||||
batch = plugin.generate_batch_code()
|
||||
|
||||
if batch is not None:
|
||||
# Return the first non-null value generated by a plugin
|
||||
return batch
|
||||
|
||||
# If we get to this point, no plugin was able to generate a new batch code
|
||||
batch_template = common.models.InvenTreeSetting.get_setting(
|
||||
'STOCK_BATCH_CODE_TEMPLATE', ''
|
||||
)
|
||||
|
||||
now = InvenTree.helpers.current_time()
|
||||
|
||||
# Pass context data through to the template rendering.
|
||||
# The following context variables are available for custom batch code generation
|
||||
context = {
|
||||
'date': now,
|
||||
'year': now.year,
|
||||
'month': now.month,
|
||||
'day': now.day,
|
||||
'hour': now.hour,
|
||||
'minute': now.minute,
|
||||
'week': now.isocalendar()[1],
|
||||
}
|
||||
|
||||
return Template(batch_template).render(context)
|
||||
|
||||
|
||||
def default_delete_on_deplete():
|
||||
"""Return a default value for the 'delete_on_deplete' field.
|
||||
|
||||
|
@ -15,11 +15,13 @@ from rest_framework.serializers import ValidationError
|
||||
from sql_util.utils import SubqueryCount, SubquerySum
|
||||
from taggit.serializers import TagListSerializerField
|
||||
|
||||
import build.models
|
||||
import common.models
|
||||
import company.models
|
||||
import InvenTree.helpers
|
||||
import InvenTree.serializers
|
||||
import InvenTree.status_codes
|
||||
import order.models
|
||||
import part.filters as part_filters
|
||||
import part.models as part_models
|
||||
import stock.filters
|
||||
@ -39,6 +41,133 @@ from .models import (
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class GenerateBatchCodeSerializer(serializers.Serializer):
|
||||
"""Serializer for generating a batch code.
|
||||
|
||||
Any of the provided write-only fields can be used for additional context.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = [
|
||||
'batch_code',
|
||||
'build_order',
|
||||
'item',
|
||||
'location',
|
||||
'part',
|
||||
'purchase_order',
|
||||
'quantity',
|
||||
]
|
||||
|
||||
read_only_fields = ['batch_code']
|
||||
|
||||
write_only_fields = [
|
||||
'build_order',
|
||||
'item',
|
||||
'location',
|
||||
'part',
|
||||
'purchase_order',
|
||||
'quantity',
|
||||
]
|
||||
|
||||
batch_code = serializers.CharField(
|
||||
read_only=True, help_text=_('Generated batch code'), label=_('Batch Code')
|
||||
)
|
||||
|
||||
build_order = serializers.PrimaryKeyRelatedField(
|
||||
queryset=build.models.Build.objects.all(),
|
||||
many=False,
|
||||
required=False,
|
||||
allow_null=True,
|
||||
label=_('Build Order'),
|
||||
help_text=_('Select build order'),
|
||||
)
|
||||
|
||||
item = serializers.PrimaryKeyRelatedField(
|
||||
queryset=StockItem.objects.all(),
|
||||
many=False,
|
||||
required=False,
|
||||
allow_null=True,
|
||||
label=_('Stock Item'),
|
||||
help_text=_('Select stock item to generate batch code for'),
|
||||
)
|
||||
|
||||
location = serializers.PrimaryKeyRelatedField(
|
||||
queryset=StockLocation.objects.all(),
|
||||
many=False,
|
||||
required=False,
|
||||
allow_null=True,
|
||||
label=_('Location'),
|
||||
help_text=_('Select location to generate batch code for'),
|
||||
)
|
||||
|
||||
part = serializers.PrimaryKeyRelatedField(
|
||||
queryset=part_models.Part.objects.all(),
|
||||
many=False,
|
||||
required=False,
|
||||
allow_null=True,
|
||||
label=_('Part'),
|
||||
help_text=_('Select part to generate batch code for'),
|
||||
)
|
||||
|
||||
purchase_order = serializers.PrimaryKeyRelatedField(
|
||||
queryset=order.models.PurchaseOrder.objects.all(),
|
||||
many=False,
|
||||
required=False,
|
||||
allow_null=True,
|
||||
label=_('Purchase Order'),
|
||||
help_text=_('Select purchase order'),
|
||||
)
|
||||
|
||||
quantity = serializers.FloatField(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
label=_('Quantity'),
|
||||
help_text=_('Enter quantity for batch code'),
|
||||
)
|
||||
|
||||
|
||||
class GenerateSerialNumberSerializer(serializers.Serializer):
|
||||
"""Serializer for generating one or multiple serial numbers.
|
||||
|
||||
Any of the provided write-only fields can be used for additional context.
|
||||
|
||||
Note that in the case where multiple serial numbers are required,
|
||||
the "serial" field will return a string with multiple serial numbers separated by a comma.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass options."""
|
||||
|
||||
fields = ['serial', 'part', 'quantity']
|
||||
|
||||
read_only_fields = ['serial']
|
||||
|
||||
write_only_fields = ['part', 'quantity']
|
||||
|
||||
serial = serializers.CharField(
|
||||
read_only=True, help_text=_('Generated serial number'), label=_('Serial Number')
|
||||
)
|
||||
|
||||
part = serializers.PrimaryKeyRelatedField(
|
||||
queryset=part_models.Part.objects.all(),
|
||||
many=False,
|
||||
required=False,
|
||||
allow_null=True,
|
||||
label=_('Part'),
|
||||
help_text=_('Select part to generate serial number for'),
|
||||
)
|
||||
|
||||
quantity = serializers.IntegerField(
|
||||
required=False,
|
||||
allow_null=False,
|
||||
default=1,
|
||||
label=_('Quantity'),
|
||||
help_text=_('Quantity of serial numbers to generate'),
|
||||
)
|
||||
|
||||
|
||||
class LocationBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
||||
"""Provides a brief serializer for a StockLocation object."""
|
||||
|
||||
|
@ -108,6 +108,10 @@ export enum ApiEndpoints {
|
||||
stock_status = 'stock/status/',
|
||||
stock_install = 'stock/:id/install',
|
||||
|
||||
// Generator API endpoints
|
||||
generate_batch_code = 'generate/batch-code/',
|
||||
generate_serial_number = 'generate/serial-number/',
|
||||
|
||||
// Order API endpoints
|
||||
purchase_order_list = 'order/po/',
|
||||
purchase_order_line_list = 'order/po-line/',
|
||||
|
@ -19,6 +19,7 @@ import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||
import { ModelType } from '../enums/ModelType';
|
||||
import { InvenTreeIcon } from '../functions/icons';
|
||||
import { useCreateApiFormModal } from '../hooks/UseForm';
|
||||
import { useBatchCodeGenerator } from '../hooks/UseGenerator';
|
||||
import { apiUrl } from '../states/ApiState';
|
||||
import { PartColumn, StatusColumn } from '../tables/ColumnRenderers';
|
||||
|
||||
@ -34,10 +35,19 @@ export function useBuildOrderFields({
|
||||
null
|
||||
);
|
||||
|
||||
const [batchCode, setBatchCode] = useState<string>('');
|
||||
|
||||
const batchGenerator = useBatchCodeGenerator((value: any) => {
|
||||
if (!batchCode) {
|
||||
setBatchCode(value);
|
||||
}
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
reference: {},
|
||||
part: {
|
||||
disabled: !create,
|
||||
filters: {
|
||||
assembly: true,
|
||||
virtual: false
|
||||
@ -49,6 +59,10 @@ export function useBuildOrderFields({
|
||||
record.default_location || record.category_default_location
|
||||
);
|
||||
}
|
||||
|
||||
batchGenerator.update({
|
||||
part: value
|
||||
});
|
||||
}
|
||||
},
|
||||
title: {},
|
||||
@ -66,7 +80,10 @@ export function useBuildOrderFields({
|
||||
sales_order: {
|
||||
icon: <IconTruckDelivery />
|
||||
},
|
||||
batch: {},
|
||||
batch: {
|
||||
value: batchCode,
|
||||
onValueChange: (value: any) => setBatchCode(value)
|
||||
},
|
||||
target_date: {
|
||||
icon: <IconCalendar />
|
||||
},
|
||||
@ -90,7 +107,7 @@ export function useBuildOrderFields({
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [create, destination]);
|
||||
}, [create, destination, batchCode]);
|
||||
}
|
||||
|
||||
export function useBuildOrderOutputFields({
|
||||
|
@ -39,6 +39,7 @@ import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||
import { ModelType } from '../enums/ModelType';
|
||||
import { InvenTreeIcon } from '../functions/icons';
|
||||
import { useCreateApiFormModal } from '../hooks/UseForm';
|
||||
import { useBatchCodeGenerator } from '../hooks/UseGenerator';
|
||||
import { apiUrl } from '../states/ApiState';
|
||||
|
||||
/*
|
||||
@ -212,6 +213,12 @@ function LineItemFormRow({
|
||||
input.changeFn(input.idx, 'location', location);
|
||||
}, [location]);
|
||||
|
||||
const batchCodeGenerator = useBatchCodeGenerator((value: any) => {
|
||||
if (!batchCode) {
|
||||
setBatchCode(value);
|
||||
}
|
||||
});
|
||||
|
||||
// State for serializing
|
||||
const [batchCode, setBatchCode] = useState<string>('');
|
||||
const [serials, setSerials] = useState<string>('');
|
||||
@ -219,6 +226,13 @@ function LineItemFormRow({
|
||||
onClose: () => {
|
||||
input.changeFn(input.idx, 'batch_code', '');
|
||||
input.changeFn(input.idx, 'serial_numbers', '');
|
||||
},
|
||||
onOpen: () => {
|
||||
// Generate a new batch code
|
||||
batchCodeGenerator.update({
|
||||
part: record?.supplier_part_detail?.part,
|
||||
order: record?.order
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -21,6 +21,10 @@ import {
|
||||
useCreateApiFormModal,
|
||||
useDeleteApiFormModal
|
||||
} from '../hooks/UseForm';
|
||||
import {
|
||||
useBatchCodeGenerator,
|
||||
useSerialNumberGenerator
|
||||
} from '../hooks/UseGenerator';
|
||||
import { apiUrl } from '../states/ApiState';
|
||||
|
||||
/**
|
||||
@ -34,15 +38,41 @@ export function useStockFields({
|
||||
const [part, setPart] = useState<number | null>(null);
|
||||
const [supplierPart, setSupplierPart] = useState<number | null>(null);
|
||||
|
||||
const [batchCode, setBatchCode] = useState<string>('');
|
||||
const [serialNumbers, setSerialNumbers] = useState<string>('');
|
||||
|
||||
const [trackable, setTrackable] = useState<boolean>(false);
|
||||
|
||||
const batchGenerator = useBatchCodeGenerator((value: any) => {
|
||||
if (!batchCode) {
|
||||
setBatchCode(value);
|
||||
}
|
||||
});
|
||||
|
||||
const serialGenerator = useSerialNumberGenerator((value: any) => {
|
||||
if (!serialNumbers && create && trackable) {
|
||||
setSerialNumbers(value);
|
||||
}
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
const fields: ApiFormFieldSet = {
|
||||
part: {
|
||||
value: part,
|
||||
disabled: !create,
|
||||
onValueChange: (change) => {
|
||||
setPart(change);
|
||||
onValueChange: (value, record) => {
|
||||
setPart(value);
|
||||
// TODO: implement remaining functionality from old stock.py
|
||||
|
||||
setTrackable(record.trackable ?? false);
|
||||
|
||||
batchGenerator.update({ part: value });
|
||||
serialGenerator.update({ part: value });
|
||||
|
||||
if (!record.trackable) {
|
||||
setSerialNumbers('');
|
||||
}
|
||||
|
||||
// Clear the 'supplier_part' field if the part is changed
|
||||
setSupplierPart(null);
|
||||
}
|
||||
@ -50,7 +80,9 @@ export function useStockFields({
|
||||
supplier_part: {
|
||||
// TODO: icon
|
||||
value: supplierPart,
|
||||
onValueChange: setSupplierPart,
|
||||
onValueChange: (value) => {
|
||||
setSupplierPart(value);
|
||||
},
|
||||
filters: {
|
||||
part_detail: true,
|
||||
supplier_detail: true,
|
||||
@ -70,22 +102,29 @@ export function useStockFields({
|
||||
},
|
||||
location: {
|
||||
hidden: !create,
|
||||
onValueChange: (value) => {
|
||||
batchGenerator.update({ location: value });
|
||||
},
|
||||
filters: {
|
||||
structural: false
|
||||
}
|
||||
// TODO: icon
|
||||
},
|
||||
quantity: {
|
||||
hidden: !create,
|
||||
description: t`Enter initial quantity for this stock item`
|
||||
description: t`Enter initial quantity for this stock item`,
|
||||
onValueChange: (value) => {
|
||||
batchGenerator.update({ quantity: value });
|
||||
}
|
||||
},
|
||||
serial_numbers: {
|
||||
// TODO: icon
|
||||
field_type: 'string',
|
||||
label: t`Serial Numbers`,
|
||||
description: t`Enter serial numbers for new stock (or leave blank)`,
|
||||
required: false,
|
||||
hidden: !create
|
||||
disabled: !trackable,
|
||||
hidden: !create,
|
||||
value: serialNumbers,
|
||||
onValueChange: (value) => setSerialNumbers(value)
|
||||
},
|
||||
serial: {
|
||||
hidden: create
|
||||
@ -93,6 +132,8 @@ export function useStockFields({
|
||||
},
|
||||
batch: {
|
||||
// TODO: icon
|
||||
value: batchCode,
|
||||
onValueChange: (value) => setBatchCode(value)
|
||||
},
|
||||
status: {},
|
||||
expiry_date: {
|
||||
@ -120,7 +161,7 @@ export function useStockFields({
|
||||
// TODO: refer to stock.py in original codebase
|
||||
|
||||
return fields;
|
||||
}, [part, supplierPart]);
|
||||
}, [part, supplierPart, batchCode, serialNumbers, trackable, create]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -194,6 +194,7 @@ const icons = {
|
||||
downright: IconCornerDownRight,
|
||||
barcode: IconQrcode,
|
||||
barLine: IconMinusVertical,
|
||||
batch: IconClipboardText,
|
||||
batch_code: IconClipboardText,
|
||||
destination: IconFlag,
|
||||
repeat_destination: IconFlagShare,
|
||||
|
90
src/frontend/src/hooks/UseGenerator.tsx
Normal file
90
src/frontend/src/hooks/UseGenerator.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { api } from '../App';
|
||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||
import { apiUrl } from '../states/ApiState';
|
||||
|
||||
export type GeneratorState = {
|
||||
query: Record<string, any>;
|
||||
result: any;
|
||||
update: (params: Record<string, any>, overwrite?: boolean) => void;
|
||||
};
|
||||
|
||||
/* Hook for managing generation of data via the InvenTree API.
|
||||
* We pass an endpoint, and start with an initially empty query.
|
||||
* We can pass additional parameters to the query, and update the query as needed.
|
||||
* Each update calls a new query to the API, and the result is stored in the state.
|
||||
*/
|
||||
export function useGenerator(
|
||||
endpoint: ApiEndpoints,
|
||||
key: string,
|
||||
onGenerate?: (value: any) => void
|
||||
): GeneratorState {
|
||||
// Track the result
|
||||
const [result, setResult] = useState<any>(null);
|
||||
|
||||
// Track the generator query
|
||||
const [query, setQuery] = useState<Record<string, any>>({});
|
||||
|
||||
// Prevent rapid updates
|
||||
const [debouncedQuery] = useDebouncedValue<Record<string, any>>(query, 250);
|
||||
|
||||
// Callback to update the generator query
|
||||
const update = useCallback(
|
||||
(params: Record<string, any>, overwrite?: boolean) => {
|
||||
if (overwrite) {
|
||||
setQuery(params);
|
||||
} else {
|
||||
setQuery((query) => ({
|
||||
...query,
|
||||
...params
|
||||
}));
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// API query handler
|
||||
const queryGenerator = useQuery({
|
||||
enabled: true,
|
||||
queryKey: ['generator', key, endpoint, debouncedQuery],
|
||||
queryFn: async () => {
|
||||
return api.post(apiUrl(endpoint), debouncedQuery).then((response) => {
|
||||
const value = response?.data[key];
|
||||
setResult(value);
|
||||
|
||||
if (onGenerate) {
|
||||
onGenerate(value);
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
query,
|
||||
update,
|
||||
result
|
||||
};
|
||||
}
|
||||
|
||||
// Generate a batch code with provided data
|
||||
export function useBatchCodeGenerator(onGenerate: (value: any) => void) {
|
||||
return useGenerator(
|
||||
ApiEndpoints.generate_batch_code,
|
||||
'batch_code',
|
||||
onGenerate
|
||||
);
|
||||
}
|
||||
|
||||
// Generate a serial number with provided data
|
||||
export function useSerialNumberGenerator(onGenerate: (value: any) => void) {
|
||||
return useGenerator(
|
||||
ApiEndpoints.generate_serial_number,
|
||||
'serial_number',
|
||||
onGenerate
|
||||
);
|
||||
}
|
@ -191,6 +191,13 @@ export default function BuildDetail() {
|
||||
model: ModelType.stocklocation,
|
||||
label: t`Destination Location`,
|
||||
hidden: !build.destination
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'batch',
|
||||
label: t`Batch Code`,
|
||||
hidden: !build.batch,
|
||||
copy: true
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -113,6 +113,9 @@ export default function BuildOutputTable({ build }: { build: any }) {
|
||||
url: apiUrl(ApiEndpoints.build_output_create, buildId),
|
||||
title: t`Add Build Output`,
|
||||
fields: buildOutputFields,
|
||||
initialData: {
|
||||
batch_code: build.batch
|
||||
},
|
||||
table: table
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user