mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 20:16:44 +00:00
[PUI] Sales order shipments (#8250)
* Refactor AttachmentPanel into common component * Remove unused imports * Add very basic implementation for SalesOrderShipmentDetail page * Refactor NotesPanel into common component * Fetch customer data * Add some placeholder actions * Updates for shipment detail page * Adjust SalesOrderShipment API * Add badges * Implement API filter for SalesOrderAllocation * Display allocation table on shipment page * Add placeholder action to edit allocations * Improvements for SalesOrderAllocationTable * Improve API db fetch efficiency * Edit / delete pending allocations * Fix for legacy CUI tables * API tweaks * Revert custom attachment code for SalesOrderShipment * Implement "complete shipment" form * Allocate stock item(s) to sales order * Fixes for TableField rendering * Reset sourceLocation when form opens * Updated playwrigh tests * Tweak branch (will be reverted) * Revert github workflow
This commit is contained in:
parent
35969b11a5
commit
33eba14d3f
@ -1,13 +1,17 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 266
|
INVENTREE_API_VERSION = 267
|
||||||
|
|
||||||
"""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."""
|
||||||
|
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
INVENTREE_API_TEXT = """
|
||||||
|
|
||||||
|
267 - 2024-10-8 : https://github.com/inventree/InvenTree/pull/8250
|
||||||
|
- Remove "allocations" field from the SalesOrderShipment API endpoint(s)
|
||||||
|
- Add "allocated_items" field to the SalesOrderShipment API endpoint(s)
|
||||||
|
|
||||||
266 - 2024-10-07 : https://github.com/inventree/InvenTree/pull/8249
|
266 - 2024-10-07 : https://github.com/inventree/InvenTree/pull/8249
|
||||||
- Tweak SalesOrderShipment API for more efficient data retrieval
|
- Tweak SalesOrderShipment API for more efficient data retrieval
|
||||||
|
|
||||||
|
@ -749,7 +749,6 @@ class SalesOrderLineItemMixin:
|
|||||||
|
|
||||||
kwargs['part_detail'] = str2bool(params.get('part_detail', False))
|
kwargs['part_detail'] = str2bool(params.get('part_detail', False))
|
||||||
kwargs['order_detail'] = str2bool(params.get('order_detail', False))
|
kwargs['order_detail'] = str2bool(params.get('order_detail', False))
|
||||||
kwargs['allocations'] = str2bool(params.get('allocations', False))
|
|
||||||
kwargs['customer_detail'] = str2bool(params.get('customer_detail', False))
|
kwargs['customer_detail'] = str2bool(params.get('customer_detail', False))
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@ -889,18 +888,83 @@ class SalesOrderAllocate(SalesOrderContextMixin, CreateAPI):
|
|||||||
serializer_class = serializers.SalesOrderShipmentAllocationSerializer
|
serializer_class = serializers.SalesOrderShipmentAllocationSerializer
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderAllocationDetail(RetrieveUpdateDestroyAPI):
|
class SalesOrderAllocationFilter(rest_filters.FilterSet):
|
||||||
"""API endpoint for detali view of a SalesOrderAllocation object."""
|
"""Custom filterset for the SalesOrderAllocationList endpoint."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Metaclass options."""
|
||||||
|
|
||||||
|
model = models.SalesOrderAllocation
|
||||||
|
fields = ['shipment', 'item']
|
||||||
|
|
||||||
|
order = rest_filters.ModelChoiceFilter(
|
||||||
|
queryset=models.SalesOrder.objects.all(),
|
||||||
|
field_name='line__order',
|
||||||
|
label=_('Order'),
|
||||||
|
)
|
||||||
|
|
||||||
|
part = rest_filters.ModelChoiceFilter(
|
||||||
|
queryset=Part.objects.all(), field_name='item__part', label=_('Part')
|
||||||
|
)
|
||||||
|
|
||||||
|
outstanding = rest_filters.BooleanFilter(
|
||||||
|
label=_('Outstanding'), method='filter_outstanding'
|
||||||
|
)
|
||||||
|
|
||||||
|
def filter_outstanding(self, queryset, name, value):
|
||||||
|
"""Filter by "outstanding" status (boolean)."""
|
||||||
|
if str2bool(value):
|
||||||
|
return queryset.filter(
|
||||||
|
line__order__status__in=SalesOrderStatusGroups.OPEN,
|
||||||
|
shipment__shipment_date=None,
|
||||||
|
)
|
||||||
|
return queryset.exclude(
|
||||||
|
shipment__shipment_date=None,
|
||||||
|
line__order__status__in=SalesOrderStatusGroups.OPEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderAllocationMixin:
|
||||||
|
"""Mixin class for SalesOrderAllocation endpoints."""
|
||||||
|
|
||||||
queryset = models.SalesOrderAllocation.objects.all()
|
queryset = models.SalesOrderAllocation.objects.all()
|
||||||
serializer_class = serializers.SalesOrderAllocationSerializer
|
serializer_class = serializers.SalesOrderAllocationSerializer
|
||||||
|
|
||||||
|
def get_queryset(self, *args, **kwargs):
|
||||||
|
"""Annotate the queryset for this endpoint."""
|
||||||
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
|
|
||||||
class SalesOrderAllocationList(ListAPI):
|
queryset = queryset.prefetch_related(
|
||||||
|
'item',
|
||||||
|
'item__sales_order',
|
||||||
|
'item__part',
|
||||||
|
'item__location',
|
||||||
|
'line__order',
|
||||||
|
'line__part',
|
||||||
|
'shipment',
|
||||||
|
'shipment__order',
|
||||||
|
)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderAllocationList(SalesOrderAllocationMixin, ListAPI):
|
||||||
"""API endpoint for listing SalesOrderAllocation objects."""
|
"""API endpoint for listing SalesOrderAllocation objects."""
|
||||||
|
|
||||||
queryset = models.SalesOrderAllocation.objects.all()
|
filterset_class = SalesOrderAllocationFilter
|
||||||
serializer_class = serializers.SalesOrderAllocationSerializer
|
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
||||||
|
|
||||||
|
ordering_fields = ['quantity', 'part', 'serial', 'batch', 'location', 'order']
|
||||||
|
|
||||||
|
ordering_field_aliases = {
|
||||||
|
'part': 'item__part__name',
|
||||||
|
'serial': ['item__serial_int', 'item__serial'],
|
||||||
|
'batch': 'item__batch',
|
||||||
|
'location': 'item__location__name',
|
||||||
|
'order': 'line__order__reference',
|
||||||
|
}
|
||||||
|
|
||||||
|
search_fields = {'item__part__name', 'item__serial', 'item__batch'}
|
||||||
|
|
||||||
def get_serializer(self, *args, **kwargs):
|
def get_serializer(self, *args, **kwargs):
|
||||||
"""Return the serializer instance for this endpoint.
|
"""Return the serializer instance for this endpoint.
|
||||||
@ -920,53 +984,9 @@ class SalesOrderAllocationList(ListAPI):
|
|||||||
|
|
||||||
return self.serializer_class(*args, **kwargs)
|
return self.serializer_class(*args, **kwargs)
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
|
||||||
"""Custom queryset filtering."""
|
|
||||||
queryset = super().filter_queryset(queryset)
|
|
||||||
|
|
||||||
# Filter by order
|
class SalesOrderAllocationDetail(SalesOrderAllocationMixin, RetrieveUpdateDestroyAPI):
|
||||||
params = self.request.query_params
|
"""API endpoint for detali view of a SalesOrderAllocation object."""
|
||||||
|
|
||||||
# Filter by "part" reference
|
|
||||||
part = params.get('part', None)
|
|
||||||
|
|
||||||
if part is not None:
|
|
||||||
queryset = queryset.filter(item__part=part)
|
|
||||||
|
|
||||||
# Filter by "order" reference
|
|
||||||
order = params.get('order', None)
|
|
||||||
|
|
||||||
if order is not None:
|
|
||||||
queryset = queryset.filter(line__order=order)
|
|
||||||
|
|
||||||
# Filter by "stock item"
|
|
||||||
item = params.get('item', params.get('stock_item', None))
|
|
||||||
|
|
||||||
if item is not None:
|
|
||||||
queryset = queryset.filter(item=item)
|
|
||||||
|
|
||||||
# Filter by "outstanding" order status
|
|
||||||
outstanding = params.get('outstanding', None)
|
|
||||||
|
|
||||||
if outstanding is not None:
|
|
||||||
outstanding = str2bool(outstanding)
|
|
||||||
|
|
||||||
if outstanding:
|
|
||||||
# Filter only "open" orders
|
|
||||||
# Filter only allocations which have *not* shipped
|
|
||||||
queryset = queryset.filter(
|
|
||||||
line__order__status__in=SalesOrderStatusGroups.OPEN,
|
|
||||||
shipment__shipment_date=None,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
queryset = queryset.exclude(
|
|
||||||
line__order__status__in=SalesOrderStatusGroups.OPEN,
|
|
||||||
shipment__shipment_date=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
filter_backends = [rest_filters.DjangoFilterBackend]
|
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderShipmentFilter(rest_filters.FilterSet):
|
class SalesOrderShipmentFilter(rest_filters.FilterSet):
|
||||||
@ -1005,13 +1025,7 @@ class SalesOrderShipmentMixin:
|
|||||||
"""Return annotated queryset for this endpoint."""
|
"""Return annotated queryset for this endpoint."""
|
||||||
queryset = super().get_queryset(*args, **kwargs)
|
queryset = super().get_queryset(*args, **kwargs)
|
||||||
|
|
||||||
queryset = queryset.prefetch_related(
|
queryset = serializers.SalesOrderShipmentSerializer.annotate_queryset(queryset)
|
||||||
'order',
|
|
||||||
'order__customer',
|
|
||||||
'allocations',
|
|
||||||
'allocations__item',
|
|
||||||
'allocations__item__part',
|
|
||||||
)
|
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
@ -1020,10 +1034,8 @@ class SalesOrderShipmentList(SalesOrderShipmentMixin, ListCreateAPI):
|
|||||||
"""API list endpoint for SalesOrderShipment model."""
|
"""API list endpoint for SalesOrderShipment model."""
|
||||||
|
|
||||||
filterset_class = SalesOrderShipmentFilter
|
filterset_class = SalesOrderShipmentFilter
|
||||||
|
|
||||||
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
filter_backends = SEARCH_ORDER_FILTER_ALIAS
|
||||||
|
ordering_fields = ['reference', 'delivery_date', 'shipment_date', 'allocated_items']
|
||||||
ordering_fields = ['delivery_date', 'shipment_date']
|
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderShipmentDetail(SalesOrderShipmentMixin, RetrieveUpdateDestroyAPI):
|
class SalesOrderShipmentDetail(SalesOrderShipmentMixin, RetrieveUpdateDestroyAPI):
|
||||||
|
@ -1923,13 +1923,6 @@ class SalesOrderShipment(
|
|||||||
|
|
||||||
trigger_event('salesordershipment.completed', id=self.pk)
|
trigger_event('salesordershipment.completed', id=self.pk)
|
||||||
|
|
||||||
def create_attachment(self, *args, **kwargs):
|
|
||||||
"""Create an attachment / link on parent order.
|
|
||||||
|
|
||||||
This will only be called when a generated report should be attached to this instance.
|
|
||||||
"""
|
|
||||||
return self.order.create_attachment(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderExtraLine(OrderExtraLine):
|
class SalesOrderExtraLine(OrderExtraLine):
|
||||||
"""Model for a single ExtraLine in a SalesOrder.
|
"""Model for a single ExtraLine in a SalesOrder.
|
||||||
|
@ -1027,88 +1027,6 @@ class SalesOrderIssueSerializer(OrderAdjustSerializer):
|
|||||||
self.order.issue_order()
|
self.order.issue_order()
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
|
||||||
"""Serializer for the SalesOrderAllocation model.
|
|
||||||
|
|
||||||
This includes some fields from the related model objects.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
"""Metaclass options."""
|
|
||||||
|
|
||||||
model = order.models.SalesOrderAllocation
|
|
||||||
|
|
||||||
fields = [
|
|
||||||
'pk',
|
|
||||||
'line',
|
|
||||||
'customer_detail',
|
|
||||||
'serial',
|
|
||||||
'quantity',
|
|
||||||
'location',
|
|
||||||
'location_detail',
|
|
||||||
'item',
|
|
||||||
'item_detail',
|
|
||||||
'order',
|
|
||||||
'order_detail',
|
|
||||||
'part',
|
|
||||||
'part_detail',
|
|
||||||
'shipment',
|
|
||||||
'shipment_date',
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
"""Initialization routine for the serializer."""
|
|
||||||
order_detail = kwargs.pop('order_detail', False)
|
|
||||||
part_detail = kwargs.pop('part_detail', True)
|
|
||||||
item_detail = kwargs.pop('item_detail', True)
|
|
||||||
location_detail = kwargs.pop('location_detail', False)
|
|
||||||
customer_detail = kwargs.pop('customer_detail', False)
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
if not order_detail:
|
|
||||||
self.fields.pop('order_detail', None)
|
|
||||||
|
|
||||||
if not part_detail:
|
|
||||||
self.fields.pop('part_detail', None)
|
|
||||||
|
|
||||||
if not item_detail:
|
|
||||||
self.fields.pop('item_detail', None)
|
|
||||||
|
|
||||||
if not location_detail:
|
|
||||||
self.fields.pop('location_detail', None)
|
|
||||||
|
|
||||||
if not customer_detail:
|
|
||||||
self.fields.pop('customer_detail', None)
|
|
||||||
|
|
||||||
part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True)
|
|
||||||
order = serializers.PrimaryKeyRelatedField(
|
|
||||||
source='line.order', many=False, read_only=True
|
|
||||||
)
|
|
||||||
serial = serializers.CharField(source='get_serial', read_only=True)
|
|
||||||
quantity = serializers.FloatField(read_only=False)
|
|
||||||
location = serializers.PrimaryKeyRelatedField(
|
|
||||||
source='item.location', many=False, read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extra detail fields
|
|
||||||
order_detail = SalesOrderSerializer(source='line.order', many=False, read_only=True)
|
|
||||||
part_detail = PartBriefSerializer(source='item.part', many=False, read_only=True)
|
|
||||||
item_detail = stock.serializers.StockItemSerializerBrief(
|
|
||||||
source='item', many=False, read_only=True
|
|
||||||
)
|
|
||||||
location_detail = stock.serializers.LocationBriefSerializer(
|
|
||||||
source='item.location', many=False, read_only=True
|
|
||||||
)
|
|
||||||
customer_detail = CompanyBriefSerializer(
|
|
||||||
source='line.order.customer', many=False, read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
shipment_date = serializers.DateField(
|
|
||||||
source='shipment.shipment_date', read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register_importer()
|
@register_importer()
|
||||||
class SalesOrderLineItemSerializer(
|
class SalesOrderLineItemSerializer(
|
||||||
DataImportExportSerializerMixin,
|
DataImportExportSerializerMixin,
|
||||||
@ -1125,7 +1043,6 @@ class SalesOrderLineItemSerializer(
|
|||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
'allocated',
|
'allocated',
|
||||||
'allocations',
|
|
||||||
'customer_detail',
|
'customer_detail',
|
||||||
'quantity',
|
'quantity',
|
||||||
'reference',
|
'reference',
|
||||||
@ -1154,7 +1071,6 @@ class SalesOrderLineItemSerializer(
|
|||||||
"""
|
"""
|
||||||
part_detail = kwargs.pop('part_detail', False)
|
part_detail = kwargs.pop('part_detail', False)
|
||||||
order_detail = kwargs.pop('order_detail', False)
|
order_detail = kwargs.pop('order_detail', False)
|
||||||
allocations = kwargs.pop('allocations', False)
|
|
||||||
customer_detail = kwargs.pop('customer_detail', False)
|
customer_detail = kwargs.pop('customer_detail', False)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -1165,9 +1081,6 @@ class SalesOrderLineItemSerializer(
|
|||||||
if order_detail is not True:
|
if order_detail is not True:
|
||||||
self.fields.pop('order_detail', None)
|
self.fields.pop('order_detail', None)
|
||||||
|
|
||||||
if allocations is not True:
|
|
||||||
self.fields.pop('allocations', None)
|
|
||||||
|
|
||||||
if customer_detail is not True:
|
if customer_detail is not True:
|
||||||
self.fields.pop('customer_detail', None)
|
self.fields.pop('customer_detail', None)
|
||||||
|
|
||||||
@ -1251,13 +1164,10 @@ class SalesOrderLineItemSerializer(
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
customer_detail = CompanyBriefSerializer(
|
|
||||||
source='order.customer', many=False, read_only=True
|
|
||||||
)
|
|
||||||
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
|
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
|
||||||
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
||||||
allocations = SalesOrderAllocationSerializer(
|
customer_detail = CompanyBriefSerializer(
|
||||||
many=True, read_only=True, location_detail=True
|
source='order.customer', many=False, read_only=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# Annotated fields
|
# Annotated fields
|
||||||
@ -1293,7 +1203,7 @@ class SalesOrderShipmentSerializer(NotesFieldMixin, InvenTreeModelSerializer):
|
|||||||
'pk',
|
'pk',
|
||||||
'order',
|
'order',
|
||||||
'order_detail',
|
'order_detail',
|
||||||
'allocations',
|
'allocated_items',
|
||||||
'shipment_date',
|
'shipment_date',
|
||||||
'delivery_date',
|
'delivery_date',
|
||||||
'checked_by',
|
'checked_by',
|
||||||
@ -1304,13 +1214,105 @@ class SalesOrderShipmentSerializer(NotesFieldMixin, InvenTreeModelSerializer):
|
|||||||
'notes',
|
'notes',
|
||||||
]
|
]
|
||||||
|
|
||||||
allocations = SalesOrderAllocationSerializer(
|
@staticmethod
|
||||||
many=True, read_only=True, location_detail=True
|
def annotate_queryset(queryset):
|
||||||
|
"""Annotate the queryset with extra information."""
|
||||||
|
# Prefetch related objects
|
||||||
|
queryset = queryset.prefetch_related('order', 'order__customer', 'allocations')
|
||||||
|
|
||||||
|
queryset = queryset.annotate(allocated_items=SubqueryCount('allocations'))
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
allocated_items = serializers.IntegerField(
|
||||||
|
read_only=True, label=_('Allocated Items')
|
||||||
)
|
)
|
||||||
|
|
||||||
order_detail = SalesOrderSerializer(source='order', read_only=True, many=False)
|
order_detail = SalesOrderSerializer(source='order', read_only=True, many=False)
|
||||||
|
|
||||||
|
|
||||||
|
class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
|
||||||
|
"""Serializer for the SalesOrderAllocation model.
|
||||||
|
|
||||||
|
This includes some fields from the related model objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
"""Metaclass options."""
|
||||||
|
|
||||||
|
model = order.models.SalesOrderAllocation
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
'pk',
|
||||||
|
'line',
|
||||||
|
'customer_detail',
|
||||||
|
'serial',
|
||||||
|
'quantity',
|
||||||
|
'location',
|
||||||
|
'location_detail',
|
||||||
|
'item',
|
||||||
|
'item_detail',
|
||||||
|
'order',
|
||||||
|
'order_detail',
|
||||||
|
'part',
|
||||||
|
'part_detail',
|
||||||
|
'shipment',
|
||||||
|
'shipment_detail',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""Initialization routine for the serializer."""
|
||||||
|
order_detail = kwargs.pop('order_detail', False)
|
||||||
|
part_detail = kwargs.pop('part_detail', True)
|
||||||
|
item_detail = kwargs.pop('item_detail', True)
|
||||||
|
location_detail = kwargs.pop('location_detail', False)
|
||||||
|
customer_detail = kwargs.pop('customer_detail', False)
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if not order_detail:
|
||||||
|
self.fields.pop('order_detail', None)
|
||||||
|
|
||||||
|
if not part_detail:
|
||||||
|
self.fields.pop('part_detail', None)
|
||||||
|
|
||||||
|
if not item_detail:
|
||||||
|
self.fields.pop('item_detail', None)
|
||||||
|
|
||||||
|
if not location_detail:
|
||||||
|
self.fields.pop('location_detail', None)
|
||||||
|
|
||||||
|
if not customer_detail:
|
||||||
|
self.fields.pop('customer_detail', None)
|
||||||
|
|
||||||
|
part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True)
|
||||||
|
order = serializers.PrimaryKeyRelatedField(
|
||||||
|
source='line.order', many=False, read_only=True
|
||||||
|
)
|
||||||
|
serial = serializers.CharField(source='get_serial', read_only=True)
|
||||||
|
quantity = serializers.FloatField(read_only=False)
|
||||||
|
location = serializers.PrimaryKeyRelatedField(
|
||||||
|
source='item.location', many=False, read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extra detail fields
|
||||||
|
order_detail = SalesOrderSerializer(source='line.order', many=False, read_only=True)
|
||||||
|
part_detail = PartBriefSerializer(source='item.part', many=False, read_only=True)
|
||||||
|
item_detail = stock.serializers.StockItemSerializerBrief(
|
||||||
|
source='item', many=False, read_only=True
|
||||||
|
)
|
||||||
|
location_detail = stock.serializers.LocationBriefSerializer(
|
||||||
|
source='item.location', many=False, read_only=True
|
||||||
|
)
|
||||||
|
customer_detail = CompanyBriefSerializer(
|
||||||
|
source='line.order.customer', many=False, read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
shipment_detail = SalesOrderShipmentSerializer(
|
||||||
|
source='shipment', many=False, read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
|
class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
|
||||||
"""Serializer for completing (shipping) a SalesOrderShipment."""
|
"""Serializer for completing (shipping) a SalesOrderShipment."""
|
||||||
|
|
||||||
|
@ -951,7 +951,7 @@ function loadSalesOrderShipmentTable(table, options={}) {
|
|||||||
html += makeIconButton('fa-truck icon-green', 'button-shipment-ship', pk, '{% trans "Complete shipment" %}');
|
html += makeIconButton('fa-truck icon-green', 'button-shipment-ship', pk, '{% trans "Complete shipment" %}');
|
||||||
}
|
}
|
||||||
|
|
||||||
var enable_delete = row.allocations && row.allocations.length == 0;
|
var enable_delete = row.allocated_items == 0;
|
||||||
|
|
||||||
html += makeDeleteButton('button-shipment-delete', pk, '{% trans "Delete shipment" %}', {disabled: !enable_delete});
|
html += makeDeleteButton('button-shipment-delete', pk, '{% trans "Delete shipment" %}', {disabled: !enable_delete});
|
||||||
|
|
||||||
@ -1004,10 +1004,19 @@ function loadSalesOrderShipmentTable(table, options={}) {
|
|||||||
detailViewByClick: false,
|
detailViewByClick: false,
|
||||||
buttons: constructExpandCollapseButtons(table),
|
buttons: constructExpandCollapseButtons(table),
|
||||||
detailFilter: function(index, row) {
|
detailFilter: function(index, row) {
|
||||||
return row.allocations.length > 0;
|
return row.allocated_items > 0;
|
||||||
},
|
},
|
||||||
detailFormatter: function(index, row, element) {
|
detailFormatter: function(index, row, element) {
|
||||||
return showAllocationSubTable(index, row, element, options);
|
return showAllocationSubTable(
|
||||||
|
index, row, element,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
queryParams: {
|
||||||
|
shipment: row.pk,
|
||||||
|
order: row.order,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onPostBody: function() {
|
onPostBody: function() {
|
||||||
setupShipmentCallbacks();
|
setupShipmentCallbacks();
|
||||||
@ -1048,17 +1057,10 @@ function loadSalesOrderShipmentTable(table, options={}) {
|
|||||||
switchable: false,
|
switchable: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'allocations',
|
field: 'allocated_items',
|
||||||
title: '{% trans "Items" %}',
|
title: '{% trans "Items" %}',
|
||||||
switchable: false,
|
switchable: false,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
formatter: function(value, row) {
|
|
||||||
if (row && row.allocations) {
|
|
||||||
return row.allocations.length;
|
|
||||||
} else {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'shipment_date',
|
field: 'shipment_date',
|
||||||
@ -1630,7 +1632,14 @@ function showAllocationSubTable(index, row, element, options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.bootstrapTable({
|
table.bootstrapTable({
|
||||||
|
url: '{% url "api-so-allocation-list" %}',
|
||||||
onPostBody: setupCallbacks,
|
onPostBody: setupCallbacks,
|
||||||
|
queryParams: {
|
||||||
|
...options.queryParams,
|
||||||
|
part_detail: true,
|
||||||
|
location_detail: true,
|
||||||
|
order_detail: true,
|
||||||
|
},
|
||||||
data: row.allocations,
|
data: row.allocations,
|
||||||
showHeader: true,
|
showHeader: true,
|
||||||
columns: [
|
columns: [
|
||||||
@ -1641,6 +1650,13 @@ function showAllocationSubTable(index, row, element, options) {
|
|||||||
return imageHoverIcon(part.thumbnail) + renderLink(part.full_name, `/part/${part.pk}/`);
|
return imageHoverIcon(part.thumbnail) + renderLink(part.full_name, `/part/${part.pk}/`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'shipment',
|
||||||
|
title: '{% trans "Shipment" %}',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
return row.shipment_detail.reference;
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'allocated',
|
field: 'allocated',
|
||||||
title: '{% trans "Stock Item" %}',
|
title: '{% trans "Stock Item" %}',
|
||||||
@ -2289,7 +2305,16 @@ function loadSalesOrderLineItemTable(table, options={}) {
|
|||||||
},
|
},
|
||||||
detailFormatter: function(index, row, element) {
|
detailFormatter: function(index, row, element) {
|
||||||
if (options.open) {
|
if (options.open) {
|
||||||
return showAllocationSubTable(index, row, element, options);
|
return showAllocationSubTable(
|
||||||
|
index, row, element,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
queryParams: {
|
||||||
|
part: row.part,
|
||||||
|
order: row.order,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return showFulfilledSubTable(index, row, element, options);
|
return showFulfilledSubTable(index, row, element, options);
|
||||||
}
|
}
|
||||||
|
@ -159,7 +159,8 @@ export function ApiFormField({
|
|||||||
adjustFilters: undefined,
|
adjustFilters: undefined,
|
||||||
adjustValue: undefined,
|
adjustValue: undefined,
|
||||||
read_only: undefined,
|
read_only: undefined,
|
||||||
children: undefined
|
children: undefined,
|
||||||
|
exclude: undefined
|
||||||
};
|
};
|
||||||
}, [fieldDefinition]);
|
}, [fieldDefinition]);
|
||||||
|
|
||||||
|
@ -220,6 +220,7 @@ export function RelatedModelField({
|
|||||||
...definition,
|
...definition,
|
||||||
onValueChange: undefined,
|
onValueChange: undefined,
|
||||||
adjustFilters: undefined,
|
adjustFilters: undefined,
|
||||||
|
exclude: undefined,
|
||||||
read_only: undefined
|
read_only: undefined
|
||||||
};
|
};
|
||||||
}, [definition]);
|
}, [definition]);
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { Trans, t } from '@lingui/macro';
|
import { Trans, t } from '@lingui/macro';
|
||||||
import { Container, Group, Table } from '@mantine/core';
|
import { Alert, Container, Group, Table } from '@mantine/core';
|
||||||
|
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { FieldValues, UseControllerReturn } from 'react-hook-form';
|
import { FieldValues, UseControllerReturn } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { identifierString } from '../../../functions/conversion';
|
||||||
import { InvenTreeIcon } from '../../../functions/icons';
|
import { InvenTreeIcon } from '../../../functions/icons';
|
||||||
import { StandaloneField } from '../StandaloneField';
|
import { StandaloneField } from '../StandaloneField';
|
||||||
import { ApiFormFieldType } from './ApiFormField';
|
import { ApiFormFieldType } from './ApiFormField';
|
||||||
@ -58,8 +60,14 @@ export function TableField({
|
|||||||
<Table highlightOnHover striped aria-label={`table-field-${field.name}`}>
|
<Table highlightOnHover striped aria-label={`table-field-${field.name}`}>
|
||||||
<Table.Thead>
|
<Table.Thead>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
{definition.headers?.map((header) => {
|
{definition.headers?.map((header, index) => {
|
||||||
return <Table.Th key={header}>{header}</Table.Th>;
|
return (
|
||||||
|
<Table.Th
|
||||||
|
key={`table-header-${identifierString(header)}-${index}`}
|
||||||
|
>
|
||||||
|
{header}
|
||||||
|
</Table.Th>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
@ -69,7 +77,17 @@ export function TableField({
|
|||||||
// Table fields require render function
|
// Table fields require render function
|
||||||
if (!definition.modelRenderer) {
|
if (!definition.modelRenderer) {
|
||||||
return (
|
return (
|
||||||
<Table.Tr key="table-row-no-renderer">{t`modelRenderer entry required for tables`}</Table.Tr>
|
<Table.Tr key="table-row-no-renderer">
|
||||||
|
<Table.Td colSpan={definition.headers?.length}>
|
||||||
|
<Alert
|
||||||
|
color="red"
|
||||||
|
title={t`Error`}
|
||||||
|
icon={<IconExclamationCircle />}
|
||||||
|
>
|
||||||
|
{`modelRenderer entry required for tables`}
|
||||||
|
</Alert>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
27
src/frontend/src/components/panels/AttachmentPanel.tsx
Normal file
27
src/frontend/src/components/panels/AttachmentPanel.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Skeleton } from '@mantine/core';
|
||||||
|
import { IconPaperclip } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
import { ModelType } from '../../enums/ModelType';
|
||||||
|
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
||||||
|
import { PanelType } from './Panel';
|
||||||
|
|
||||||
|
export default function AttachmentPanel({
|
||||||
|
model_type,
|
||||||
|
model_id
|
||||||
|
}: {
|
||||||
|
model_type: ModelType;
|
||||||
|
model_id: number | undefined;
|
||||||
|
}): PanelType {
|
||||||
|
return {
|
||||||
|
name: 'attachments',
|
||||||
|
label: t`Attachments`,
|
||||||
|
icon: <IconPaperclip />,
|
||||||
|
content:
|
||||||
|
model_type && model_id ? (
|
||||||
|
<AttachmentTable model_type={model_type} model_id={model_id} />
|
||||||
|
) : (
|
||||||
|
<Skeleton />
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
37
src/frontend/src/components/panels/NotesPanel.tsx
Normal file
37
src/frontend/src/components/panels/NotesPanel.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Skeleton } from '@mantine/core';
|
||||||
|
import { IconNotes } from '@tabler/icons-react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { ModelType } from '../../enums/ModelType';
|
||||||
|
import { useUserState } from '../../states/UserState';
|
||||||
|
import NotesEditor from '../editors/NotesEditor';
|
||||||
|
import { PanelType } from './Panel';
|
||||||
|
|
||||||
|
export default function NotesPanel({
|
||||||
|
model_type,
|
||||||
|
model_id,
|
||||||
|
editable
|
||||||
|
}: {
|
||||||
|
model_type: ModelType;
|
||||||
|
model_id: number | undefined;
|
||||||
|
editable?: boolean;
|
||||||
|
}): PanelType {
|
||||||
|
const user = useUserState.getState();
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'notes',
|
||||||
|
label: t`Notes`,
|
||||||
|
icon: <IconNotes />,
|
||||||
|
content:
|
||||||
|
model_type && model_id ? (
|
||||||
|
<NotesEditor
|
||||||
|
modelType={model_type}
|
||||||
|
modelId={model_id}
|
||||||
|
editable={editable ?? user.hasChangePermission(model_type)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Skeleton />
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
@ -28,7 +28,7 @@ import { usePluginPanels } from '../../hooks/UsePluginPanels';
|
|||||||
import { useLocalState } from '../../states/LocalState';
|
import { useLocalState } from '../../states/LocalState';
|
||||||
import { Boundary } from '../Boundary';
|
import { Boundary } from '../Boundary';
|
||||||
import { StylishText } from '../items/StylishText';
|
import { StylishText } from '../items/StylishText';
|
||||||
import { PanelType } from './Panel';
|
import { PanelType } from '../panels/Panel';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set of properties which define a panel group:
|
* Set of properties which define a panel group:
|
@ -166,8 +166,8 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
salesordershipment: {
|
salesordershipment: {
|
||||||
label: () => t`Sales Order Shipment`,
|
label: () => t`Sales Order Shipment`,
|
||||||
label_multiple: () => t`Sales Order Shipments`,
|
label_multiple: () => t`Sales Order Shipments`,
|
||||||
url_overview: '/salesordershipment',
|
url_overview: '/sales/shipment/',
|
||||||
url_detail: '/salesordershipment/:pk/',
|
url_detail: '/sales/shipment/:pk/',
|
||||||
api_endpoint: ApiEndpoints.sales_order_shipment_list
|
api_endpoint: ApiEndpoints.sales_order_shipment_list
|
||||||
},
|
},
|
||||||
returnorder: {
|
returnorder: {
|
||||||
|
@ -113,12 +113,12 @@ export function RenderSalesOrderShipment({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
instance: any;
|
instance: any;
|
||||||
}>): ReactNode {
|
}>): ReactNode {
|
||||||
let order = instance.sales_order_detail || {};
|
let order = instance.order_detail || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RenderInlineModel
|
<RenderInlineModel
|
||||||
primary={order.reference}
|
primary={order.reference}
|
||||||
secondary={t`Shipment` + ` ${instance.description}`}
|
secondary={t`Shipment` + ` ${instance.reference}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -163,9 +163,12 @@ export enum ApiEndpoints {
|
|||||||
sales_order_line_list = 'order/so-line/',
|
sales_order_line_list = 'order/so-line/',
|
||||||
sales_order_extra_line_list = 'order/so-extra-line/',
|
sales_order_extra_line_list = 'order/so-extra-line/',
|
||||||
sales_order_allocation_list = 'order/so-allocation/',
|
sales_order_allocation_list = 'order/so-allocation/',
|
||||||
sales_order_shipment_list = 'order/so/shipment/',
|
sales_order_allocate = 'order/so/:id/allocate/',
|
||||||
sales_order_allocate_serials = 'order/so/:id/allocate-serials/',
|
sales_order_allocate_serials = 'order/so/:id/allocate-serials/',
|
||||||
|
|
||||||
|
sales_order_shipment_list = 'order/so/shipment/',
|
||||||
|
sales_order_shipment_complete = 'order/so/shipment/:id/ship/',
|
||||||
|
|
||||||
return_order_list = 'order/ro/',
|
return_order_list = 'order/ro/',
|
||||||
return_order_issue = 'order/ro/:id/issue/',
|
return_order_issue = 'order/ro/:id/issue/',
|
||||||
return_order_hold = 'order/ro/:id/hold/',
|
return_order_hold = 'order/ro/:id/hold/',
|
||||||
|
@ -423,15 +423,17 @@ function BuildAllocateLineRow({
|
|||||||
if (instance) {
|
if (instance) {
|
||||||
let available = instance.quantity - instance.allocated;
|
let available = instance.quantity - instance.allocated;
|
||||||
|
|
||||||
props.changeFn(
|
if (available < props.item.quantity) {
|
||||||
props.idx,
|
props.changeFn(
|
||||||
'quantity',
|
props.idx,
|
||||||
Math.min(props.item.quantity, available)
|
'quantity',
|
||||||
);
|
Math.min(props.item.quantity, available)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [props]);
|
}, [record, props]);
|
||||||
|
|
||||||
const quantityField: ApiFormFieldType = useMemo(() => {
|
const quantityField: ApiFormFieldType = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
|
@ -1,10 +1,22 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Table } from '@mantine/core';
|
||||||
import { IconAddressBook, IconUser, IconUsers } from '@tabler/icons-react';
|
import { IconAddressBook, IconUser, IconUsers } from '@tabler/icons-react';
|
||||||
import { useMemo } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import RemoveRowButton from '../components/buttons/RemoveRowButton';
|
||||||
|
import { StandaloneField } from '../components/forms/StandaloneField';
|
||||||
import {
|
import {
|
||||||
ApiFormAdjustFilterType,
|
ApiFormAdjustFilterType,
|
||||||
ApiFormFieldSet
|
ApiFormFieldSet,
|
||||||
|
ApiFormFieldType
|
||||||
} from '../components/forms/fields/ApiFormField';
|
} from '../components/forms/fields/ApiFormField';
|
||||||
|
import { TableFieldRowProps } from '../components/forms/fields/TableField';
|
||||||
|
import { ProgressBar } from '../components/items/ProgressBar';
|
||||||
|
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||||
|
import { ModelType } from '../enums/ModelType';
|
||||||
|
import { useCreateApiFormModal } from '../hooks/UseForm';
|
||||||
|
import { apiUrl } from '../states/ApiState';
|
||||||
|
import { PartColumn } from '../tables/ColumnRenderers';
|
||||||
|
|
||||||
export function useSalesOrderFields({
|
export function useSalesOrderFields({
|
||||||
duplicateOrderId
|
duplicateOrderId
|
||||||
@ -105,6 +117,179 @@ export function useSalesOrderLineItemFields({
|
|||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SalesOrderAllocateLineRow({
|
||||||
|
props,
|
||||||
|
record,
|
||||||
|
sourceLocation
|
||||||
|
}: {
|
||||||
|
props: TableFieldRowProps;
|
||||||
|
record: any;
|
||||||
|
sourceLocation?: number | null;
|
||||||
|
}) {
|
||||||
|
// Statically defined field for selecting the stock item
|
||||||
|
const stockItemField: ApiFormFieldType = useMemo(() => {
|
||||||
|
return {
|
||||||
|
field_type: 'related field',
|
||||||
|
api_url: apiUrl(ApiEndpoints.stock_item_list),
|
||||||
|
model: ModelType.stockitem,
|
||||||
|
filters: {
|
||||||
|
available: true,
|
||||||
|
part_detail: true,
|
||||||
|
location_detail: true,
|
||||||
|
location: sourceLocation,
|
||||||
|
cascade: sourceLocation ? true : undefined,
|
||||||
|
part: record.part
|
||||||
|
},
|
||||||
|
value: props.item.stock_item,
|
||||||
|
name: 'stock_item',
|
||||||
|
onValueChange: (value: any, instance: any) => {
|
||||||
|
props.changeFn(props.idx, 'stock_item', value);
|
||||||
|
|
||||||
|
// Update the allocated quantity based on the selected stock item
|
||||||
|
if (instance) {
|
||||||
|
let available = instance.quantity - instance.allocated;
|
||||||
|
let required = record.quantity - record.allocated;
|
||||||
|
|
||||||
|
let quantity = props.item?.quantity ?? 0;
|
||||||
|
|
||||||
|
quantity = Math.max(quantity, required);
|
||||||
|
quantity = Math.min(quantity, available);
|
||||||
|
|
||||||
|
if (quantity != props.item.quantity) {
|
||||||
|
props.changeFn(props.idx, 'quantity', quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [sourceLocation, record, props]);
|
||||||
|
|
||||||
|
// Statically defined field for selecting the allocation quantity
|
||||||
|
const quantityField: ApiFormFieldType = useMemo(() => {
|
||||||
|
return {
|
||||||
|
field_type: 'number',
|
||||||
|
name: 'quantity',
|
||||||
|
required: true,
|
||||||
|
value: props.item.quantity,
|
||||||
|
onValueChange: (value: any) => {
|
||||||
|
props.changeFn(props.idx, 'quantity', value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [props]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table.Tr key={`table-row-${props.idx}-${record.pk}`}>
|
||||||
|
<Table.Td>
|
||||||
|
<PartColumn part={record.part_detail} />
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<ProgressBar
|
||||||
|
value={record.allocated}
|
||||||
|
maximum={record.quantity}
|
||||||
|
progressLabel
|
||||||
|
/>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<StandaloneField
|
||||||
|
fieldName="stock_item"
|
||||||
|
fieldDefinition={stockItemField}
|
||||||
|
error={props.rowErrors?.stock_item?.message}
|
||||||
|
/>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<StandaloneField
|
||||||
|
fieldName="quantity"
|
||||||
|
fieldDefinition={quantityField}
|
||||||
|
error={props.rowErrors?.quantity?.message}
|
||||||
|
/>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAllocateToSalesOrderForm({
|
||||||
|
orderId,
|
||||||
|
shipmentId,
|
||||||
|
lineItems,
|
||||||
|
onFormSuccess
|
||||||
|
}: {
|
||||||
|
orderId: number;
|
||||||
|
shipmentId?: number;
|
||||||
|
lineItems: any[];
|
||||||
|
onFormSuccess: (response: any) => void;
|
||||||
|
}) {
|
||||||
|
const [sourceLocation, setSourceLocation] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Reset source location to known state
|
||||||
|
useEffect(() => {
|
||||||
|
setSourceLocation(null);
|
||||||
|
}, [orderId, shipmentId, lineItems]);
|
||||||
|
|
||||||
|
const fields: ApiFormFieldSet = useMemo(() => {
|
||||||
|
return {
|
||||||
|
// Non-submitted field to select the source location
|
||||||
|
source_location: {
|
||||||
|
exclude: true,
|
||||||
|
required: false,
|
||||||
|
field_type: 'related field',
|
||||||
|
api_url: apiUrl(ApiEndpoints.stock_location_list),
|
||||||
|
model: ModelType.stocklocation,
|
||||||
|
label: t`Source Location`,
|
||||||
|
description: t`Select the source location for the stock allocation`,
|
||||||
|
onValueChange: (value: any) => {
|
||||||
|
setSourceLocation(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
field_type: 'table',
|
||||||
|
value: [],
|
||||||
|
headers: [t`Part`, t`Allocated`, t`Stock Item`, t`Quantity`],
|
||||||
|
modelRenderer: (row: TableFieldRowProps) => {
|
||||||
|
const record =
|
||||||
|
lineItems.find((item) => item.pk == row.item.line_item) ?? {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SalesOrderAllocateLineRow
|
||||||
|
key={`table-row-${row.idx}-${record.pk}`}
|
||||||
|
props={row}
|
||||||
|
record={record}
|
||||||
|
sourceLocation={sourceLocation}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shipment: {
|
||||||
|
filters: {
|
||||||
|
shipped: false,
|
||||||
|
order_detail: true,
|
||||||
|
order: orderId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [orderId, shipmentId, lineItems, sourceLocation]);
|
||||||
|
|
||||||
|
return useCreateApiFormModal({
|
||||||
|
title: t`Allocate Stock`,
|
||||||
|
url: ApiEndpoints.sales_order_allocate,
|
||||||
|
pk: orderId,
|
||||||
|
fields: fields,
|
||||||
|
onFormSuccess: onFormSuccess,
|
||||||
|
successMessage: t`Stock items allocated`,
|
||||||
|
size: '80%',
|
||||||
|
initialData: {
|
||||||
|
items: lineItems.map((item) => {
|
||||||
|
return {
|
||||||
|
line_item: item.pk,
|
||||||
|
quantity: 0,
|
||||||
|
stock_item: null
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useSalesOrderAllocateSerialsFields({
|
export function useSalesOrderAllocateSerialsFields({
|
||||||
itemId,
|
itemId,
|
||||||
orderId
|
orderId
|
||||||
@ -122,6 +307,7 @@ export function useSalesOrderAllocateSerialsFields({
|
|||||||
serial_numbers: {},
|
serial_numbers: {},
|
||||||
shipment: {
|
shipment: {
|
||||||
filters: {
|
filters: {
|
||||||
|
order_detail: true,
|
||||||
order: orderId,
|
order: orderId,
|
||||||
shipped: false
|
shipped: false
|
||||||
}
|
}
|
||||||
@ -130,19 +316,53 @@ export function useSalesOrderAllocateSerialsFields({
|
|||||||
}, [itemId, orderId]);
|
}, [itemId, orderId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSalesOrderShipmentFields(): ApiFormFieldSet {
|
export function useSalesOrderShipmentFields({
|
||||||
|
pending
|
||||||
|
}: {
|
||||||
|
pending?: boolean;
|
||||||
|
}): ApiFormFieldSet {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return {
|
return {
|
||||||
order: {
|
order: {
|
||||||
disabled: true
|
disabled: true
|
||||||
},
|
},
|
||||||
reference: {},
|
reference: {},
|
||||||
shipment_date: {},
|
shipment_date: {
|
||||||
delivery_date: {},
|
hidden: pending ?? true
|
||||||
|
},
|
||||||
|
delivery_date: {
|
||||||
|
hidden: pending ?? true
|
||||||
|
},
|
||||||
tracking_number: {},
|
tracking_number: {},
|
||||||
invoice_number: {},
|
invoice_number: {},
|
||||||
link: {},
|
link: {}
|
||||||
notes: {}
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, [pending]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSalesOrderShipmentCompleteFields({
|
||||||
|
shipmentId
|
||||||
|
}: {
|
||||||
|
shipmentId?: number;
|
||||||
|
}): ApiFormFieldSet {
|
||||||
|
return useMemo(() => {
|
||||||
|
return {
|
||||||
|
shipment_date: {},
|
||||||
|
tracking_number: {},
|
||||||
|
invoice_number: {},
|
||||||
|
link: {}
|
||||||
|
};
|
||||||
|
}, [shipmentId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSalesOrderAllocationFields({
|
||||||
|
shipmentId
|
||||||
|
}: {
|
||||||
|
shipmentId?: number;
|
||||||
|
}): ApiFormFieldSet {
|
||||||
|
return useMemo(() => {
|
||||||
|
return {
|
||||||
|
quantity: {}
|
||||||
|
};
|
||||||
|
}, [shipmentId]);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { api } from '../App';
|
import { api } from '../App';
|
||||||
import { PanelType } from '../components/nav/Panel';
|
import { PanelType } from '../components/panels/Panel';
|
||||||
import {
|
import {
|
||||||
InvenTreeContext,
|
InvenTreeContext,
|
||||||
useInvenTreeContext
|
useInvenTreeContext
|
||||||
|
@ -30,9 +30,9 @@ import { lazy, useMemo } from 'react';
|
|||||||
|
|
||||||
import PermissionDenied from '../../../../components/errors/PermissionDenied';
|
import PermissionDenied from '../../../../components/errors/PermissionDenied';
|
||||||
import { PlaceholderPill } from '../../../../components/items/Placeholder';
|
import { PlaceholderPill } from '../../../../components/items/Placeholder';
|
||||||
import { PanelType } from '../../../../components/nav/Panel';
|
|
||||||
import { PanelGroup } from '../../../../components/nav/PanelGroup';
|
|
||||||
import { SettingsHeader } from '../../../../components/nav/SettingsHeader';
|
import { SettingsHeader } from '../../../../components/nav/SettingsHeader';
|
||||||
|
import { PanelType } from '../../../../components/panels/Panel';
|
||||||
|
import { PanelGroup } from '../../../../components/panels/PanelGroup';
|
||||||
import { GlobalSettingList } from '../../../../components/settings/SettingList';
|
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';
|
||||||
|
@ -19,9 +19,9 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
import PermissionDenied from '../../../components/errors/PermissionDenied';
|
import PermissionDenied from '../../../components/errors/PermissionDenied';
|
||||||
import { PlaceholderPanel } from '../../../components/items/Placeholder';
|
import { PlaceholderPanel } from '../../../components/items/Placeholder';
|
||||||
import { PanelType } from '../../../components/nav/Panel';
|
|
||||||
import { PanelGroup } from '../../../components/nav/PanelGroup';
|
|
||||||
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
|
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
|
||||||
|
import { PanelType } from '../../../components/panels/Panel';
|
||||||
|
import { PanelGroup } from '../../../components/panels/PanelGroup';
|
||||||
import { GlobalSettingList } from '../../../components/settings/SettingList';
|
import { GlobalSettingList } from '../../../components/settings/SettingList';
|
||||||
import { useServerApiState } from '../../../states/ApiState';
|
import { useServerApiState } from '../../../states/ApiState';
|
||||||
import { useUserState } from '../../../states/UserState';
|
import { useUserState } from '../../../states/UserState';
|
||||||
|
@ -11,9 +11,9 @@ import {
|
|||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { PanelType } from '../../../components/nav/Panel';
|
|
||||||
import { PanelGroup } from '../../../components/nav/PanelGroup';
|
|
||||||
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
|
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
|
||||||
|
import { PanelType } from '../../../components/panels/Panel';
|
||||||
|
import { PanelGroup } from '../../../components/panels/PanelGroup';
|
||||||
import { UserSettingList } from '../../../components/settings/SettingList';
|
import { UserSettingList } from '../../../components/settings/SettingList';
|
||||||
import { useUserState } from '../../../states/UserState';
|
import { useUserState } from '../../../states/UserState';
|
||||||
import { SecurityContent } from './AccountSettings/SecurityContent';
|
import { SecurityContent } from './AccountSettings/SecurityContent';
|
||||||
|
@ -14,7 +14,7 @@ import { useCallback, useMemo } from 'react';
|
|||||||
import { api } from '../App';
|
import { api } from '../App';
|
||||||
import { ActionButton } from '../components/buttons/ActionButton';
|
import { ActionButton } from '../components/buttons/ActionButton';
|
||||||
import { PageDetail } from '../components/nav/PageDetail';
|
import { PageDetail } from '../components/nav/PageDetail';
|
||||||
import { PanelGroup } from '../components/nav/PanelGroup';
|
import { PanelGroup } from '../components/panels/PanelGroup';
|
||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||||
import { useTable } from '../hooks/UseTable';
|
import { useTable } from '../hooks/UseTable';
|
||||||
import { apiUrl } from '../states/ApiState';
|
import { apiUrl } from '../states/ApiState';
|
||||||
|
@ -8,8 +8,6 @@ import {
|
|||||||
IconList,
|
IconList,
|
||||||
IconListCheck,
|
IconListCheck,
|
||||||
IconListNumbers,
|
IconListNumbers,
|
||||||
IconNotes,
|
|
||||||
IconPaperclip,
|
|
||||||
IconReportAnalytics,
|
IconReportAnalytics,
|
||||||
IconSitemap
|
IconSitemap
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
@ -22,7 +20,6 @@ import { PrintingActions } from '../../components/buttons/PrintingActions';
|
|||||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import NotesEditor from '../../components/editors/NotesEditor';
|
|
||||||
import {
|
import {
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
CancelItemAction,
|
CancelItemAction,
|
||||||
@ -33,8 +30,10 @@ import {
|
|||||||
} from '../../components/items/ActionDropdown';
|
} from '../../components/items/ActionDropdown';
|
||||||
import InstanceDetail from '../../components/nav/InstanceDetail';
|
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelType } from '../../components/nav/Panel';
|
import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||||
import { PanelGroup } from '../../components/nav/PanelGroup';
|
import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
|
import { PanelType } from '../../components/panels/Panel';
|
||||||
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
@ -53,7 +52,6 @@ import BuildLineTable from '../../tables/build/BuildLineTable';
|
|||||||
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
||||||
import BuildOrderTestTable from '../../tables/build/BuildOrderTestTable';
|
import BuildOrderTestTable from '../../tables/build/BuildOrderTestTable';
|
||||||
import BuildOutputTable from '../../tables/build/BuildOutputTable';
|
import BuildOutputTable from '../../tables/build/BuildOutputTable';
|
||||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
|
||||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||||
import { TestStatisticsTable } from '../../tables/stock/TestStatisticsTable';
|
import { TestStatisticsTable } from '../../tables/stock/TestStatisticsTable';
|
||||||
|
|
||||||
@ -343,26 +341,14 @@ export default function BuildDetail() {
|
|||||||
),
|
),
|
||||||
hidden: !build?.part_detail?.testable
|
hidden: !build?.part_detail?.testable
|
||||||
},
|
},
|
||||||
{
|
AttachmentPanel({
|
||||||
name: 'attachments',
|
model_type: ModelType.build,
|
||||||
label: t`Attachments`,
|
model_id: build.pk
|
||||||
icon: <IconPaperclip />,
|
}),
|
||||||
content: (
|
NotesPanel({
|
||||||
<AttachmentTable model_type={ModelType.build} model_id={Number(id)} />
|
model_type: ModelType.build,
|
||||||
)
|
model_id: build.pk
|
||||||
},
|
})
|
||||||
{
|
|
||||||
name: 'notes',
|
|
||||||
label: t`Notes`,
|
|
||||||
icon: <IconNotes />,
|
|
||||||
content: (
|
|
||||||
<NotesEditor
|
|
||||||
modelType={ModelType.build}
|
|
||||||
modelId={build.pk}
|
|
||||||
editable={user.hasChangeRole(UserRoles.build)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
}, [build, id, user]);
|
}, [build, id, user]);
|
||||||
|
|
||||||
|
@ -5,10 +5,8 @@ import {
|
|||||||
IconBuildingWarehouse,
|
IconBuildingWarehouse,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
IconMap2,
|
IconMap2,
|
||||||
IconNotes,
|
|
||||||
IconPackageExport,
|
IconPackageExport,
|
||||||
IconPackages,
|
IconPackages,
|
||||||
IconPaperclip,
|
|
||||||
IconShoppingCart,
|
IconShoppingCart,
|
||||||
IconTruckDelivery,
|
IconTruckDelivery,
|
||||||
IconTruckReturn,
|
IconTruckReturn,
|
||||||
@ -22,7 +20,6 @@ import { DetailsField, DetailsTable } from '../../components/details/Details';
|
|||||||
import DetailsBadge from '../../components/details/DetailsBadge';
|
import DetailsBadge from '../../components/details/DetailsBadge';
|
||||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import NotesEditor from '../../components/editors/NotesEditor';
|
|
||||||
import {
|
import {
|
||||||
DeleteItemAction,
|
DeleteItemAction,
|
||||||
EditItemAction,
|
EditItemAction,
|
||||||
@ -31,8 +28,10 @@ import {
|
|||||||
import { Breadcrumb } from '../../components/nav/BreadcrumbList';
|
import { Breadcrumb } from '../../components/nav/BreadcrumbList';
|
||||||
import InstanceDetail from '../../components/nav/InstanceDetail';
|
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelType } from '../../components/nav/Panel';
|
import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||||
import { PanelGroup } from '../../components/nav/PanelGroup';
|
import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
|
import { PanelType } from '../../components/panels/Panel';
|
||||||
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
@ -46,7 +45,6 @@ import { apiUrl } from '../../states/ApiState';
|
|||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { AddressTable } from '../../tables/company/AddressTable';
|
import { AddressTable } from '../../tables/company/AddressTable';
|
||||||
import { ContactTable } from '../../tables/company/ContactTable';
|
import { ContactTable } from '../../tables/company/ContactTable';
|
||||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
|
||||||
import { ManufacturerPartTable } from '../../tables/purchasing/ManufacturerPartTable';
|
import { ManufacturerPartTable } from '../../tables/purchasing/ManufacturerPartTable';
|
||||||
import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
|
import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
|
||||||
import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable';
|
import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable';
|
||||||
@ -256,33 +254,14 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
|||||||
icon: <IconMap2 />,
|
icon: <IconMap2 />,
|
||||||
content: company?.pk && <AddressTable companyId={company.pk} />
|
content: company?.pk && <AddressTable companyId={company.pk} />
|
||||||
},
|
},
|
||||||
{
|
AttachmentPanel({
|
||||||
name: 'attachments',
|
model_type: ModelType.company,
|
||||||
label: t`Attachments`,
|
model_id: company.pk
|
||||||
icon: <IconPaperclip />,
|
}),
|
||||||
content: (
|
NotesPanel({
|
||||||
<AttachmentTable
|
model_type: ModelType.company,
|
||||||
model_type={ModelType.company}
|
model_id: company.pk
|
||||||
model_id={company.pk}
|
})
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'notes',
|
|
||||||
label: t`Notes`,
|
|
||||||
icon: <IconNotes />,
|
|
||||||
content: (
|
|
||||||
<NotesEditor
|
|
||||||
modelType={ModelType.company}
|
|
||||||
modelId={company.pk}
|
|
||||||
editable={
|
|
||||||
user.hasChangeRole(UserRoles.purchase_order) ||
|
|
||||||
user.hasChangeRole(UserRoles.sales_order) ||
|
|
||||||
user.hasChangeRole(UserRoles.return_order)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
}, [id, company, user]);
|
}, [id, company, user]);
|
||||||
|
|
||||||
|
@ -3,9 +3,7 @@ import { Grid, Skeleton, Stack } from '@mantine/core';
|
|||||||
import {
|
import {
|
||||||
IconBuildingWarehouse,
|
IconBuildingWarehouse,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
IconList,
|
IconList
|
||||||
IconNotes,
|
|
||||||
IconPaperclip
|
|
||||||
} from '@tabler/icons-react';
|
} 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';
|
||||||
@ -14,7 +12,6 @@ import AdminButton from '../../components/buttons/AdminButton';
|
|||||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import NotesEditor from '../../components/editors/NotesEditor';
|
|
||||||
import {
|
import {
|
||||||
DeleteItemAction,
|
DeleteItemAction,
|
||||||
DuplicateItemAction,
|
DuplicateItemAction,
|
||||||
@ -23,8 +20,10 @@ import {
|
|||||||
} from '../../components/items/ActionDropdown';
|
} from '../../components/items/ActionDropdown';
|
||||||
import InstanceDetail from '../../components/nav/InstanceDetail';
|
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelType } from '../../components/nav/Panel';
|
import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||||
import { PanelGroup } from '../../components/nav/PanelGroup';
|
import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
|
import { PanelType } from '../../components/panels/Panel';
|
||||||
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
@ -38,7 +37,6 @@ import {
|
|||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
|
||||||
import ManufacturerPartParameterTable from '../../tables/purchasing/ManufacturerPartParameterTable';
|
import ManufacturerPartParameterTable from '../../tables/purchasing/ManufacturerPartParameterTable';
|
||||||
import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable';
|
import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable';
|
||||||
|
|
||||||
@ -187,29 +185,14 @@ export default function ManufacturerPartDetail() {
|
|||||||
<Skeleton />
|
<Skeleton />
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
AttachmentPanel({
|
||||||
name: 'attachments',
|
model_type: ModelType.manufacturerpart,
|
||||||
label: t`Attachments`,
|
model_id: manufacturerPart?.pk
|
||||||
icon: <IconPaperclip />,
|
}),
|
||||||
content: (
|
NotesPanel({
|
||||||
<AttachmentTable
|
model_type: ModelType.manufacturerpart,
|
||||||
model_type={ModelType.manufacturerpart}
|
model_id: manufacturerPart?.pk
|
||||||
model_id={manufacturerPart?.pk}
|
})
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'notes',
|
|
||||||
label: t`Notes`,
|
|
||||||
icon: <IconNotes />,
|
|
||||||
content: (
|
|
||||||
<NotesEditor
|
|
||||||
modelType={ModelType.manufacturerpart}
|
|
||||||
modelId={manufacturerPart.pk}
|
|
||||||
editable={user.hasChangeRole(UserRoles.purchase_order)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
}, [manufacturerPart]);
|
}, [manufacturerPart]);
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ import { Grid, Skeleton, Stack } from '@mantine/core';
|
|||||||
import {
|
import {
|
||||||
IconCurrencyDollar,
|
IconCurrencyDollar,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
IconNotes,
|
|
||||||
IconPackages,
|
IconPackages,
|
||||||
IconShoppingCart
|
IconShoppingCart
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
@ -15,7 +14,6 @@ import { DetailsField, DetailsTable } from '../../components/details/Details';
|
|||||||
import DetailsBadge from '../../components/details/DetailsBadge';
|
import DetailsBadge from '../../components/details/DetailsBadge';
|
||||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import NotesEditor from '../../components/editors/NotesEditor';
|
|
||||||
import {
|
import {
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
DeleteItemAction,
|
DeleteItemAction,
|
||||||
@ -25,8 +23,9 @@ import {
|
|||||||
} from '../../components/items/ActionDropdown';
|
} from '../../components/items/ActionDropdown';
|
||||||
import InstanceDetail from '../../components/nav/InstanceDetail';
|
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelType } from '../../components/nav/Panel';
|
import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
import { PanelGroup } from '../../components/nav/PanelGroup';
|
import { PanelType } from '../../components/panels/Panel';
|
||||||
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
@ -257,18 +256,10 @@ export default function SupplierPartDetail() {
|
|||||||
<Skeleton />
|
<Skeleton />
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
NotesPanel({
|
||||||
name: 'notes',
|
model_type: ModelType.supplierpart,
|
||||||
label: t`Notes`,
|
model_id: supplierPart?.pk
|
||||||
icon: <IconNotes />,
|
})
|
||||||
content: (
|
|
||||||
<NotesEditor
|
|
||||||
modelType={ModelType.supplierpart}
|
|
||||||
modelId={supplierPart.pk}
|
|
||||||
editable={user.hasChangeRole(UserRoles.purchase_order)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
}, [supplierPart]);
|
}, [supplierPart]);
|
||||||
|
|
||||||
|
@ -21,8 +21,8 @@ import { ApiIcon } from '../../components/items/ApiIcon';
|
|||||||
import InstanceDetail from '../../components/nav/InstanceDetail';
|
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||||
import NavigationTree from '../../components/nav/NavigationTree';
|
import NavigationTree from '../../components/nav/NavigationTree';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelType } from '../../components/nav/Panel';
|
import { PanelType } from '../../components/panels/Panel';
|
||||||
import { PanelGroup } from '../../components/nav/PanelGroup';
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
|
@ -59,8 +59,10 @@ import { StylishText } from '../../components/items/StylishText';
|
|||||||
import InstanceDetail from '../../components/nav/InstanceDetail';
|
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||||
import NavigationTree from '../../components/nav/NavigationTree';
|
import NavigationTree from '../../components/nav/NavigationTree';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelType } from '../../components/nav/Panel';
|
import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||||
import { PanelGroup } from '../../components/nav/PanelGroup';
|
import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
|
import { PanelType } from '../../components/panels/Panel';
|
||||||
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import { RenderPart } from '../../components/render/Part';
|
import { RenderPart } from '../../components/render/Part';
|
||||||
import { formatPriceRange } from '../../defaults/formatters';
|
import { formatPriceRange } from '../../defaults/formatters';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
@ -90,7 +92,6 @@ import { BomTable } from '../../tables/bom/BomTable';
|
|||||||
import { UsedInTable } from '../../tables/bom/UsedInTable';
|
import { UsedInTable } from '../../tables/bom/UsedInTable';
|
||||||
import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable';
|
import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable';
|
||||||
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
||||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
|
||||||
import { PartParameterTable } from '../../tables/part/PartParameterTable';
|
import { PartParameterTable } from '../../tables/part/PartParameterTable';
|
||||||
import PartPurchaseOrdersTable from '../../tables/part/PartPurchaseOrdersTable';
|
import PartPurchaseOrdersTable from '../../tables/part/PartPurchaseOrdersTable';
|
||||||
import PartTestTemplateTable from '../../tables/part/PartTestTemplateTable';
|
import PartTestTemplateTable from '../../tables/part/PartTestTemplateTable';
|
||||||
@ -742,26 +743,14 @@ export default function PartDetail() {
|
|||||||
icon: <IconLayersLinked />,
|
icon: <IconLayersLinked />,
|
||||||
content: <RelatedPartTable partId={part.pk ?? -1} />
|
content: <RelatedPartTable partId={part.pk ?? -1} />
|
||||||
},
|
},
|
||||||
{
|
AttachmentPanel({
|
||||||
name: 'attachments',
|
model_type: ModelType.part,
|
||||||
label: t`Attachments`,
|
model_id: part?.pk
|
||||||
icon: <IconPaperclip />,
|
}),
|
||||||
content: (
|
NotesPanel({
|
||||||
<AttachmentTable model_type={ModelType.part} model_id={part?.pk} />
|
model_type: ModelType.part,
|
||||||
)
|
model_id: part?.pk
|
||||||
},
|
})
|
||||||
{
|
|
||||||
name: 'notes',
|
|
||||||
label: t`Notes`,
|
|
||||||
icon: <IconNotes />,
|
|
||||||
content: (
|
|
||||||
<NotesEditor
|
|
||||||
modelType={ModelType.part}
|
|
||||||
modelId={part.pk}
|
|
||||||
editable={user.hasChangeRole(UserRoles.part)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
}, [id, part, user, globalSettings, userSettings]);
|
}, [id, part, user, globalSettings, userSettings]);
|
||||||
|
|
||||||
|
@ -1,12 +1,6 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Accordion, Grid, Skeleton, Stack } from '@mantine/core';
|
import { Accordion, Grid, Skeleton, Stack } from '@mantine/core';
|
||||||
import {
|
import { IconInfoCircle, IconList, IconPackages } from '@tabler/icons-react';
|
||||||
IconInfoCircle,
|
|
||||||
IconList,
|
|
||||||
IconNotes,
|
|
||||||
IconPackages,
|
|
||||||
IconPaperclip
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { ReactNode, useMemo } from 'react';
|
import { ReactNode, useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
@ -16,7 +10,6 @@ import { PrintingActions } from '../../components/buttons/PrintingActions';
|
|||||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import NotesEditor from '../../components/editors/NotesEditor';
|
|
||||||
import {
|
import {
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
CancelItemAction,
|
CancelItemAction,
|
||||||
@ -28,8 +21,10 @@ import {
|
|||||||
import { StylishText } from '../../components/items/StylishText';
|
import { StylishText } from '../../components/items/StylishText';
|
||||||
import InstanceDetail from '../../components/nav/InstanceDetail';
|
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelType } from '../../components/nav/Panel';
|
import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||||
import { PanelGroup } from '../../components/nav/PanelGroup';
|
import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
|
import { PanelType } from '../../components/panels/Panel';
|
||||||
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||||
import { formatCurrency } from '../../defaults/formatters';
|
import { formatCurrency } from '../../defaults/formatters';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
@ -45,7 +40,6 @@ import useStatusCodes from '../../hooks/UseStatusCodes';
|
|||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useGlobalSettingsState } from '../../states/SettingsState';
|
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
|
||||||
import ExtraLineItemTable from '../../tables/general/ExtraLineItemTable';
|
import ExtraLineItemTable from '../../tables/general/ExtraLineItemTable';
|
||||||
import { PurchaseOrderLineItemTable } from '../../tables/purchasing/PurchaseOrderLineItemTable';
|
import { PurchaseOrderLineItemTable } from '../../tables/purchasing/PurchaseOrderLineItemTable';
|
||||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||||
@ -304,29 +298,14 @@ export default function PurchaseOrderDetail() {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
AttachmentPanel({
|
||||||
name: 'attachments',
|
model_type: ModelType.purchaseorder,
|
||||||
label: t`Attachments`,
|
model_id: order.pk
|
||||||
icon: <IconPaperclip />,
|
}),
|
||||||
content: (
|
NotesPanel({
|
||||||
<AttachmentTable
|
model_type: ModelType.purchaseorder,
|
||||||
model_type={ModelType.purchaseorder}
|
model_id: order.pk
|
||||||
model_id={order.pk}
|
})
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'notes',
|
|
||||||
label: t`Notes`,
|
|
||||||
icon: <IconNotes />,
|
|
||||||
content: (
|
|
||||||
<NotesEditor
|
|
||||||
modelType={ModelType.purchaseorder}
|
|
||||||
modelId={order.pk}
|
|
||||||
editable={user.hasChangeRole(UserRoles.purchase_order)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
}, [order, id, user]);
|
}, [order, id, user]);
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
import PermissionDenied from '../../components/errors/PermissionDenied';
|
import PermissionDenied from '../../components/errors/PermissionDenied';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelGroup } from '../../components/nav/PanelGroup';
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { CompanyTable } from '../../tables/company/CompanyTable';
|
import { CompanyTable } from '../../tables/company/CompanyTable';
|
||||||
|
@ -1,11 +1,6 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Accordion, Grid, Skeleton, Stack } from '@mantine/core';
|
import { Accordion, Grid, Skeleton, Stack } from '@mantine/core';
|
||||||
import {
|
import { IconInfoCircle, IconList } from '@tabler/icons-react';
|
||||||
IconInfoCircle,
|
|
||||||
IconList,
|
|
||||||
IconNotes,
|
|
||||||
IconPaperclip
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { ReactNode, useMemo } from 'react';
|
import { ReactNode, useMemo } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
@ -15,7 +10,6 @@ import { PrintingActions } from '../../components/buttons/PrintingActions';
|
|||||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import NotesEditor from '../../components/editors/NotesEditor';
|
|
||||||
import {
|
import {
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
CancelItemAction,
|
CancelItemAction,
|
||||||
@ -27,8 +21,10 @@ import {
|
|||||||
import { StylishText } from '../../components/items/StylishText';
|
import { StylishText } from '../../components/items/StylishText';
|
||||||
import InstanceDetail from '../../components/nav/InstanceDetail';
|
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelType } from '../../components/nav/Panel';
|
import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||||
import { PanelGroup } from '../../components/nav/PanelGroup';
|
import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
|
import { PanelType } from '../../components/panels/Panel';
|
||||||
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||||
import { formatCurrency } from '../../defaults/formatters';
|
import { formatCurrency } from '../../defaults/formatters';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
@ -44,7 +40,6 @@ import useStatusCodes from '../../hooks/UseStatusCodes';
|
|||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useGlobalSettingsState } from '../../states/SettingsState';
|
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
|
||||||
import ExtraLineItemTable from '../../tables/general/ExtraLineItemTable';
|
import ExtraLineItemTable from '../../tables/general/ExtraLineItemTable';
|
||||||
import ReturnOrderLineItemTable from '../../tables/sales/ReturnOrderLineItemTable';
|
import ReturnOrderLineItemTable from '../../tables/sales/ReturnOrderLineItemTable';
|
||||||
|
|
||||||
@ -267,29 +262,14 @@ export default function ReturnOrderDetail() {
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
AttachmentPanel({
|
||||||
name: 'attachments',
|
model_type: ModelType.returnorder,
|
||||||
label: t`Attachments`,
|
model_id: order.pk
|
||||||
icon: <IconPaperclip />,
|
}),
|
||||||
content: (
|
NotesPanel({
|
||||||
<AttachmentTable
|
model_type: ModelType.returnorder,
|
||||||
model_type={ModelType.returnorder}
|
model_id: order.pk
|
||||||
model_id={order.pk}
|
})
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'notes',
|
|
||||||
label: t`Notes`,
|
|
||||||
icon: <IconNotes />,
|
|
||||||
content: (
|
|
||||||
<NotesEditor
|
|
||||||
modelType={ModelType.returnorder}
|
|
||||||
modelId={order.pk}
|
|
||||||
editable={user.hasChangeRole(UserRoles.return_order)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
}, [order, id, user]);
|
}, [order, id, user]);
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
import PermissionDenied from '../../components/errors/PermissionDenied';
|
import PermissionDenied from '../../components/errors/PermissionDenied';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelGroup } from '../../components/nav/PanelGroup';
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { CompanyTable } from '../../tables/company/CompanyTable';
|
import { CompanyTable } from '../../tables/company/CompanyTable';
|
||||||
|
@ -4,8 +4,6 @@ import {
|
|||||||
IconBookmark,
|
IconBookmark,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
IconList,
|
IconList,
|
||||||
IconNotes,
|
|
||||||
IconPaperclip,
|
|
||||||
IconTools,
|
IconTools,
|
||||||
IconTruckDelivery
|
IconTruckDelivery
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
@ -18,7 +16,6 @@ import { PrintingActions } from '../../components/buttons/PrintingActions';
|
|||||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import NotesEditor from '../../components/editors/NotesEditor';
|
|
||||||
import {
|
import {
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
CancelItemAction,
|
CancelItemAction,
|
||||||
@ -30,8 +27,10 @@ import {
|
|||||||
import { StylishText } from '../../components/items/StylishText';
|
import { StylishText } from '../../components/items/StylishText';
|
||||||
import InstanceDetail from '../../components/nav/InstanceDetail';
|
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelType } from '../../components/nav/Panel';
|
import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||||
import { PanelGroup } from '../../components/nav/PanelGroup';
|
import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
|
import { PanelType } from '../../components/panels/Panel';
|
||||||
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||||
import { formatCurrency } from '../../defaults/formatters';
|
import { formatCurrency } from '../../defaults/formatters';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
@ -48,7 +47,6 @@ import { apiUrl } from '../../states/ApiState';
|
|||||||
import { useGlobalSettingsState } from '../../states/SettingsState';
|
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
||||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
|
||||||
import ExtraLineItemTable from '../../tables/general/ExtraLineItemTable';
|
import ExtraLineItemTable from '../../tables/general/ExtraLineItemTable';
|
||||||
import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable';
|
import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable';
|
||||||
import SalesOrderLineItemTable from '../../tables/sales/SalesOrderLineItemTable';
|
import SalesOrderLineItemTable from '../../tables/sales/SalesOrderLineItemTable';
|
||||||
@ -338,29 +336,14 @@ export default function SalesOrderDetail() {
|
|||||||
<Skeleton />
|
<Skeleton />
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
AttachmentPanel({
|
||||||
name: 'attachments',
|
model_type: ModelType.salesorder,
|
||||||
label: t`Attachments`,
|
model_id: order.pk
|
||||||
icon: <IconPaperclip />,
|
}),
|
||||||
content: (
|
NotesPanel({
|
||||||
<AttachmentTable
|
model_type: ModelType.salesorder,
|
||||||
model_type={ModelType.salesorder}
|
model_id: order.pk
|
||||||
model_id={order.pk}
|
})
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'notes',
|
|
||||||
label: t`Notes`,
|
|
||||||
icon: <IconNotes />,
|
|
||||||
content: (
|
|
||||||
<NotesEditor
|
|
||||||
modelType={ModelType.salesorder}
|
|
||||||
modelId={order.pk}
|
|
||||||
editable={user.hasChangeRole(UserRoles.sales_order)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
}, [order, id, user, soStatus]);
|
}, [order, id, user, soStatus]);
|
||||||
|
|
||||||
|
366
src/frontend/src/pages/sales/SalesOrderShipmentDetail.tsx
Normal file
366
src/frontend/src/pages/sales/SalesOrderShipmentDetail.tsx
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||||
|
import { IconInfoCircle, IconPackages } from '@tabler/icons-react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import PrimaryActionButton from '../../components/buttons/PrimaryActionButton';
|
||||||
|
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||||
|
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||||
|
import DetailsBadge from '../../components/details/DetailsBadge';
|
||||||
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
|
import {
|
||||||
|
BarcodeActionDropdown,
|
||||||
|
CancelItemAction,
|
||||||
|
EditItemAction,
|
||||||
|
OptionsActionDropdown
|
||||||
|
} from '../../components/items/ActionDropdown';
|
||||||
|
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||||
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
|
import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||||
|
import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
|
import { PanelType } from '../../components/panels/Panel';
|
||||||
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
|
import { formatDate } from '../../defaults/formatters';
|
||||||
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
|
import { ModelType } from '../../enums/ModelType';
|
||||||
|
import { UserRoles } from '../../enums/Roles';
|
||||||
|
import {
|
||||||
|
useSalesOrderShipmentCompleteFields,
|
||||||
|
useSalesOrderShipmentFields
|
||||||
|
} from '../../forms/SalesOrderForms';
|
||||||
|
import { getDetailUrl } from '../../functions/urls';
|
||||||
|
import {
|
||||||
|
useCreateApiFormModal,
|
||||||
|
useDeleteApiFormModal,
|
||||||
|
useEditApiFormModal
|
||||||
|
} from '../../hooks/UseForm';
|
||||||
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
|
import { useUserState } from '../../states/UserState';
|
||||||
|
import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable';
|
||||||
|
|
||||||
|
export default function SalesOrderShipmentDetail() {
|
||||||
|
const { id } = useParams();
|
||||||
|
const user = useUserState();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const {
|
||||||
|
instance: shipment,
|
||||||
|
instanceQuery: shipmentQuery,
|
||||||
|
refreshInstance: refreshShipment,
|
||||||
|
requestStatus: shipmentStatus
|
||||||
|
} = useInstance({
|
||||||
|
endpoint: ApiEndpoints.sales_order_shipment_list,
|
||||||
|
pk: id,
|
||||||
|
params: {
|
||||||
|
order_detail: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
instance: customer,
|
||||||
|
instanceQuery: customerQuery,
|
||||||
|
refreshInstance: refreshCustomer,
|
||||||
|
requestStatus: customerStatus
|
||||||
|
} = useInstance({
|
||||||
|
endpoint: ApiEndpoints.company_list,
|
||||||
|
pk: shipment.order_detail?.customer,
|
||||||
|
hasPrimaryKey: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPending = useMemo(() => !shipment.shipment_date, [shipment]);
|
||||||
|
|
||||||
|
const detailsPanel = useMemo(() => {
|
||||||
|
if (shipmentQuery.isFetching || customerQuery.isFetching) {
|
||||||
|
return <Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: any = {
|
||||||
|
...shipment,
|
||||||
|
customer: customer?.pk,
|
||||||
|
customer_name: customer?.name,
|
||||||
|
customer_reference: shipment.order_detail?.customer_reference
|
||||||
|
};
|
||||||
|
|
||||||
|
// Top Left: Order / customer information
|
||||||
|
let tl: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
model: ModelType.salesorder,
|
||||||
|
name: 'order',
|
||||||
|
label: t`Sales Order`,
|
||||||
|
icon: 'sales_orders',
|
||||||
|
model_field: 'reference'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
name: 'customer',
|
||||||
|
icon: 'customers',
|
||||||
|
label: t`Customer`,
|
||||||
|
model: ModelType.company,
|
||||||
|
model_field: 'name',
|
||||||
|
hidden: !data.customer
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'customer_reference',
|
||||||
|
icon: 'serial',
|
||||||
|
label: t`Customer Reference`,
|
||||||
|
hidden: !data.customer_reference,
|
||||||
|
copy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'reference',
|
||||||
|
icon: 'serial',
|
||||||
|
label: t`Shipment Reference`,
|
||||||
|
copy: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'allocated_items',
|
||||||
|
icon: 'packages',
|
||||||
|
label: t`Allocated Items`
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Top right: Shipment information
|
||||||
|
let tr: DetailsField[] = [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'tracking_number',
|
||||||
|
label: t`Tracking Number`,
|
||||||
|
icon: 'trackable',
|
||||||
|
value_formatter: () => shipment.tracking_number || '---',
|
||||||
|
copy: !!shipment.tracking_number
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'invoice_number',
|
||||||
|
label: t`Invoice Number`,
|
||||||
|
icon: 'serial',
|
||||||
|
value_formatter: () => shipment.invoice_number || '---',
|
||||||
|
copy: !!shipment.invoice_number
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'shipment_date',
|
||||||
|
label: t`Shipment Date`,
|
||||||
|
icon: 'calendar',
|
||||||
|
value_formatter: () => formatDate(shipment.shipment_date),
|
||||||
|
hidden: !shipment.shipment_date
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
name: 'delivery_date',
|
||||||
|
label: t`Delivery Date`,
|
||||||
|
icon: 'calendar',
|
||||||
|
value_formatter: () => formatDate(shipment.delivery_date),
|
||||||
|
hidden: !shipment.delivery_date
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
external: true,
|
||||||
|
name: 'link',
|
||||||
|
label: t`Link`,
|
||||||
|
copy: true,
|
||||||
|
hidden: !shipment.link
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ItemDetailsGrid>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<DetailsImage
|
||||||
|
appRole={UserRoles.sales_order}
|
||||||
|
apiPath={ApiEndpoints.company_list}
|
||||||
|
src={customer?.image}
|
||||||
|
pk={customer?.pk}
|
||||||
|
imageActions={{
|
||||||
|
selectExisting: false,
|
||||||
|
downloadImage: false,
|
||||||
|
uploadFile: false,
|
||||||
|
deleteFile: false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<DetailsTable fields={tl} item={data} />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
<DetailsTable fields={tr} item={data} />
|
||||||
|
</ItemDetailsGrid>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}, [shipment, shipmentQuery, customer, customerQuery]);
|
||||||
|
|
||||||
|
const shipmentPanels: PanelType[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'detail',
|
||||||
|
label: t`Shipment Details`,
|
||||||
|
icon: <IconInfoCircle />,
|
||||||
|
content: detailsPanel
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'items',
|
||||||
|
label: t`Assigned Items`,
|
||||||
|
icon: <IconPackages />,
|
||||||
|
content: (
|
||||||
|
<SalesOrderAllocationTable
|
||||||
|
shipmentId={shipment.pk}
|
||||||
|
showPartInfo
|
||||||
|
allowEdit={isPending}
|
||||||
|
modelField="item"
|
||||||
|
modelTarget={ModelType.stockitem}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
AttachmentPanel({
|
||||||
|
model_type: ModelType.salesordershipment,
|
||||||
|
model_id: shipment.pk
|
||||||
|
}),
|
||||||
|
NotesPanel({
|
||||||
|
model_type: ModelType.salesordershipment,
|
||||||
|
model_id: shipment.pk
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}, [isPending, shipment, detailsPanel]);
|
||||||
|
|
||||||
|
const editShipmentFields = useSalesOrderShipmentFields({
|
||||||
|
pending: isPending
|
||||||
|
});
|
||||||
|
|
||||||
|
const editShipment = useEditApiFormModal({
|
||||||
|
url: ApiEndpoints.sales_order_shipment_list,
|
||||||
|
pk: shipment.pk,
|
||||||
|
fields: editShipmentFields,
|
||||||
|
title: t`Edit Shipment`,
|
||||||
|
onFormSuccess: refreshShipment
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteShipment = useDeleteApiFormModal({
|
||||||
|
url: ApiEndpoints.sales_order_shipment_list,
|
||||||
|
pk: shipment.pk,
|
||||||
|
title: t`Cancel Shipment`,
|
||||||
|
onFormSuccess: () => {
|
||||||
|
// Shipment has been deleted - navigate back to the sales order
|
||||||
|
navigate(getDetailUrl(ModelType.salesorder, shipment.order));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const completeShipmentFields = useSalesOrderShipmentCompleteFields({});
|
||||||
|
|
||||||
|
const completeShipment = useCreateApiFormModal({
|
||||||
|
url: ApiEndpoints.sales_order_shipment_complete,
|
||||||
|
pk: shipment.pk,
|
||||||
|
fields: completeShipmentFields,
|
||||||
|
title: t`Complete Shipment`,
|
||||||
|
focus: 'tracking_number',
|
||||||
|
initialData: {
|
||||||
|
...shipment,
|
||||||
|
shipment_date: new Date().toISOString().split('T')[0]
|
||||||
|
},
|
||||||
|
onFormSuccess: refreshShipment
|
||||||
|
});
|
||||||
|
|
||||||
|
const shipmentBadges = useMemo(() => {
|
||||||
|
if (shipmentQuery.isFetching) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
<DetailsBadge label={t`Pending`} color="gray" visible={isPending} />,
|
||||||
|
<DetailsBadge label={t`Shipped`} color="green" visible={!isPending} />,
|
||||||
|
<DetailsBadge
|
||||||
|
label={t`Delivered`}
|
||||||
|
color="blue"
|
||||||
|
visible={!!shipment.delivery_date}
|
||||||
|
/>
|
||||||
|
];
|
||||||
|
}, [shipment, shipmentQuery]);
|
||||||
|
|
||||||
|
const shipmentActions = useMemo(() => {
|
||||||
|
const canEdit: boolean = user.hasChangePermission(
|
||||||
|
ModelType.salesordershipment
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
<PrimaryActionButton
|
||||||
|
title={t`Send Shipment`}
|
||||||
|
icon="sales_orders"
|
||||||
|
hidden={!isPending}
|
||||||
|
color="green"
|
||||||
|
onClick={() => {
|
||||||
|
completeShipment.open();
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
<BarcodeActionDropdown
|
||||||
|
model={ModelType.salesordershipment}
|
||||||
|
pk={shipment.pk}
|
||||||
|
/>,
|
||||||
|
<PrintingActions
|
||||||
|
modelType={ModelType.salesordershipment}
|
||||||
|
items={[shipment.pk]}
|
||||||
|
enableLabels
|
||||||
|
enableReports
|
||||||
|
/>,
|
||||||
|
<OptionsActionDropdown
|
||||||
|
tooltip={t`Shipment Actions`}
|
||||||
|
actions={[
|
||||||
|
EditItemAction({
|
||||||
|
hidden: !canEdit,
|
||||||
|
onClick: editShipment.open,
|
||||||
|
tooltip: t`Edit Shipment`
|
||||||
|
}),
|
||||||
|
CancelItemAction({
|
||||||
|
hidden: !isPending,
|
||||||
|
onClick: deleteShipment.open,
|
||||||
|
tooltip: t`Cancel Shipment`
|
||||||
|
})
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
];
|
||||||
|
}, [isPending, user, shipment]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{completeShipment.modal}
|
||||||
|
{editShipment.modal}
|
||||||
|
{deleteShipment.modal}
|
||||||
|
<InstanceDetail
|
||||||
|
status={shipmentStatus}
|
||||||
|
loading={shipmentQuery.isFetching || customerQuery.isFetching}
|
||||||
|
>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<PageDetail
|
||||||
|
title={t`Sales Order Shipment` + `: ${shipment.reference}`}
|
||||||
|
subtitle={t`Sales Order` + `: ${shipment.order_detail?.reference}`}
|
||||||
|
breadcrumbs={[
|
||||||
|
{ name: t`Sales`, url: '/sales/' },
|
||||||
|
{
|
||||||
|
name: shipment.order_detail?.reference,
|
||||||
|
url: getDetailUrl(ModelType.salesorder, shipment.order)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
badges={shipmentBadges}
|
||||||
|
imageUrl={customer?.image}
|
||||||
|
editAction={editShipment.open}
|
||||||
|
editEnabled={user.hasChangePermission(ModelType.salesordershipment)}
|
||||||
|
actions={shipmentActions}
|
||||||
|
/>
|
||||||
|
<PanelGroup
|
||||||
|
pageKey="salesordershipment"
|
||||||
|
panels={shipmentPanels}
|
||||||
|
model={ModelType.salesordershipment}
|
||||||
|
instance={shipment}
|
||||||
|
id={shipment.pk}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</InstanceDetail>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -20,8 +20,8 @@ import { ApiIcon } from '../../components/items/ApiIcon';
|
|||||||
import InstanceDetail from '../../components/nav/InstanceDetail';
|
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||||
import NavigationTree from '../../components/nav/NavigationTree';
|
import NavigationTree from '../../components/nav/NavigationTree';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelType } from '../../components/nav/Panel';
|
import { PanelType } from '../../components/panels/Panel';
|
||||||
import { PanelGroup } from '../../components/nav/PanelGroup';
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
|
@ -6,9 +6,7 @@ import {
|
|||||||
IconChecklist,
|
IconChecklist,
|
||||||
IconHistory,
|
IconHistory,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
IconNotes,
|
|
||||||
IconPackages,
|
IconPackages,
|
||||||
IconPaperclip,
|
|
||||||
IconSitemap
|
IconSitemap
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
@ -22,7 +20,6 @@ import { DetailsField, DetailsTable } from '../../components/details/Details';
|
|||||||
import DetailsBadge from '../../components/details/DetailsBadge';
|
import DetailsBadge from '../../components/details/DetailsBadge';
|
||||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||||
import NotesEditor from '../../components/editors/NotesEditor';
|
|
||||||
import {
|
import {
|
||||||
ActionDropdown,
|
ActionDropdown,
|
||||||
BarcodeActionDropdown,
|
BarcodeActionDropdown,
|
||||||
@ -35,8 +32,10 @@ import { StylishText } from '../../components/items/StylishText';
|
|||||||
import InstanceDetail from '../../components/nav/InstanceDetail';
|
import InstanceDetail from '../../components/nav/InstanceDetail';
|
||||||
import NavigationTree from '../../components/nav/NavigationTree';
|
import NavigationTree from '../../components/nav/NavigationTree';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelType } from '../../components/nav/Panel';
|
import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||||
import { PanelGroup } from '../../components/nav/PanelGroup';
|
import NotesPanel from '../../components/panels/NotesPanel';
|
||||||
|
import { PanelType } from '../../components/panels/Panel';
|
||||||
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||||
import { formatCurrency } from '../../defaults/formatters';
|
import { formatCurrency } from '../../defaults/formatters';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
@ -62,7 +61,6 @@ import { useInstance } from '../../hooks/UseInstance';
|
|||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable';
|
import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable';
|
||||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
|
||||||
import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable';
|
import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable';
|
||||||
import InstalledItemsTable from '../../tables/stock/InstalledItemsTable';
|
import InstalledItemsTable from '../../tables/stock/InstalledItemsTable';
|
||||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||||
@ -480,29 +478,14 @@ export default function StockDetail() {
|
|||||||
<Skeleton />
|
<Skeleton />
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
AttachmentPanel({
|
||||||
name: 'attachments',
|
model_type: ModelType.stockitem,
|
||||||
label: t`Attachments`,
|
model_id: stockitem.pk
|
||||||
icon: <IconPaperclip />,
|
}),
|
||||||
content: (
|
NotesPanel({
|
||||||
<AttachmentTable
|
model_type: ModelType.stockitem,
|
||||||
model_type={ModelType.stockitem}
|
model_id: stockitem.pk
|
||||||
model_id={stockitem.pk}
|
})
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'notes',
|
|
||||||
label: t`Notes`,
|
|
||||||
icon: <IconNotes />,
|
|
||||||
content: (
|
|
||||||
<NotesEditor
|
|
||||||
modelType={ModelType.stockitem}
|
|
||||||
modelId={stockitem.pk}
|
|
||||||
editable={user.hasChangeRole(UserRoles.stock)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
}, [
|
}, [
|
||||||
showSalesAlloctions,
|
showSalesAlloctions,
|
||||||
|
@ -74,6 +74,10 @@ export const SalesOrderDetail = Loadable(
|
|||||||
lazy(() => import('./pages/sales/SalesOrderDetail'))
|
lazy(() => import('./pages/sales/SalesOrderDetail'))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const SalesOrderShipmentDetail = Loadable(
|
||||||
|
lazy(() => import('./pages/sales/SalesOrderShipmentDetail'))
|
||||||
|
);
|
||||||
|
|
||||||
export const ReturnOrderDetail = Loadable(
|
export const ReturnOrderDetail = Loadable(
|
||||||
lazy(() => import('./pages/sales/ReturnOrderDetail'))
|
lazy(() => import('./pages/sales/ReturnOrderDetail'))
|
||||||
);
|
);
|
||||||
@ -160,6 +164,7 @@ export const routes = (
|
|||||||
<Route index element={<Navigate to="index/" />} />
|
<Route index element={<Navigate to="index/" />} />
|
||||||
<Route path="index/*" element={<SalesIndex />} />
|
<Route path="index/*" element={<SalesIndex />} />
|
||||||
<Route path="sales-order/:id/*" element={<SalesOrderDetail />} />
|
<Route path="sales-order/:id/*" element={<SalesOrderDetail />} />
|
||||||
|
<Route path="shipment/:id/*" element={<SalesOrderShipmentDetail />} />
|
||||||
<Route path="return-order/:id/*" element={<ReturnOrderDetail />} />
|
<Route path="return-order/:id/*" element={<ReturnOrderDetail />} />
|
||||||
<Route path="customer/:id/*" element={<CustomerDetail />} />
|
<Route path="customer/:id/*" element={<CustomerDetail />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { ActionIcon, Menu, Tooltip } from '@mantine/core';
|
import { ActionIcon, Menu, Tooltip } from '@mantine/core';
|
||||||
import { IconCopy, IconDots, IconEdit, IconTrash } from '@tabler/icons-react';
|
import {
|
||||||
|
IconCircleX,
|
||||||
|
IconCopy,
|
||||||
|
IconDots,
|
||||||
|
IconEdit,
|
||||||
|
IconTrash
|
||||||
|
} from '@tabler/icons-react';
|
||||||
import { ReactNode, useMemo, useState } from 'react';
|
import { ReactNode, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { cancelEvent } from '../functions/events';
|
import { cancelEvent } from '../functions/events';
|
||||||
@ -11,7 +17,7 @@ export type RowAction = {
|
|||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
onClick: () => void;
|
onClick: (event: any) => void;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
@ -46,6 +52,16 @@ export function RowDeleteAction(props: RowAction): RowAction {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Component for cancelling a row in a table
|
||||||
|
export function RowCancelAction(props: RowAction): RowAction {
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
title: t`Cancel`,
|
||||||
|
color: 'red',
|
||||||
|
icon: <IconCircleX />
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for displaying actions for a row in a table.
|
* Component for displaying actions for a row in a table.
|
||||||
* Displays a simple dropdown menu with a list of actions.
|
* Displays a simple dropdown menu with a list of actions.
|
||||||
@ -89,7 +105,7 @@ export function RowActions({
|
|||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
// Prevent clicking on the action from selecting the row itself
|
// Prevent clicking on the action from selecting the row itself
|
||||||
cancelEvent(event);
|
cancelEvent(event);
|
||||||
action.onClick();
|
action.onClick(event);
|
||||||
setOpened(false);
|
setOpened(false);
|
||||||
}}
|
}}
|
||||||
disabled={action.disabled || false}
|
disabled={action.disabled || false}
|
||||||
|
@ -316,7 +316,7 @@ export default function BuildLineTable({
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
const allowcateStock = useAllocateStockToBuildForm({
|
const allocateStock = useAllocateStockToBuildForm({
|
||||||
build: build,
|
build: build,
|
||||||
outputId: null,
|
outputId: null,
|
||||||
buildId: build.pk,
|
buildId: build.pk,
|
||||||
@ -395,7 +395,7 @@ export default function BuildLineTable({
|
|||||||
color: 'green',
|
color: 'green',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setSelectedRows([record]);
|
setSelectedRows([record]);
|
||||||
allowcateStock.open();
|
allocateStock.open();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -465,7 +465,7 @@ export default function BuildLineTable({
|
|||||||
!r.bom_item_detail.consumable
|
!r.bom_item_detail.consumable
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
allowcateStock.open();
|
allocateStock.open();
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
<ActionButton
|
<ActionButton
|
||||||
@ -493,7 +493,7 @@ export default function BuildLineTable({
|
|||||||
<>
|
<>
|
||||||
{autoAllocateStock.modal}
|
{autoAllocateStock.modal}
|
||||||
{newBuildOrder.modal}
|
{newBuildOrder.modal}
|
||||||
{allowcateStock.modal}
|
{allocateStock.modal}
|
||||||
{deallocateStock.modal}
|
{deallocateStock.modal}
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url={apiUrl(ApiEndpoints.build_line_list)}
|
url={apiUrl(ApiEndpoints.build_line_list)}
|
||||||
|
@ -179,6 +179,7 @@ export default function BuildOutputTable({
|
|||||||
url: apiUrl(ApiEndpoints.build_output_create, buildId),
|
url: apiUrl(ApiEndpoints.build_output_create, buildId),
|
||||||
title: t`Add Build Output`,
|
title: t`Add Build Output`,
|
||||||
fields: buildOutputFields,
|
fields: buildOutputFields,
|
||||||
|
timeout: 10000,
|
||||||
initialData: {
|
initialData: {
|
||||||
batch_code: build.batch,
|
batch_code: build.batch,
|
||||||
location: build.destination ?? build.part_detail?.default_location
|
location: build.destination ?? build.part_detail?.default_location
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||||
|
import { YesNoButton } from '../../components/buttons/YesNoButton';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
|
import { UserRoles } from '../../enums/Roles';
|
||||||
|
import { useSalesOrderAllocationFields } from '../../forms/SalesOrderForms';
|
||||||
|
import {
|
||||||
|
useDeleteApiFormModal,
|
||||||
|
useEditApiFormModal
|
||||||
|
} from '../../hooks/UseForm';
|
||||||
import { useTable } from '../../hooks/UseTable';
|
import { useTable } from '../../hooks/UseTable';
|
||||||
import { apiUrl } from '../../states/ApiState';
|
import { apiUrl } from '../../states/ApiState';
|
||||||
import { useUserState } from '../../states/UserState';
|
import { useUserState } from '../../states/UserState';
|
||||||
@ -15,12 +23,13 @@ import {
|
|||||||
} from '../ColumnRenderers';
|
} from '../ColumnRenderers';
|
||||||
import { TableFilter } from '../Filter';
|
import { TableFilter } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
import { RowAction } from '../RowActions';
|
import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
|
||||||
|
|
||||||
export default function SalesOrderAllocationTable({
|
export default function SalesOrderAllocationTable({
|
||||||
partId,
|
partId,
|
||||||
stockId,
|
stockId,
|
||||||
orderId,
|
orderId,
|
||||||
|
shipmentId,
|
||||||
showPartInfo,
|
showPartInfo,
|
||||||
showOrderInfo,
|
showOrderInfo,
|
||||||
allowEdit,
|
allowEdit,
|
||||||
@ -30,6 +39,7 @@ export default function SalesOrderAllocationTable({
|
|||||||
partId?: number;
|
partId?: number;
|
||||||
stockId?: number;
|
stockId?: number;
|
||||||
orderId?: number;
|
orderId?: number;
|
||||||
|
shipmentId?: number;
|
||||||
showPartInfo?: boolean;
|
showPartInfo?: boolean;
|
||||||
showOrderInfo?: boolean;
|
showOrderInfo?: boolean;
|
||||||
allowEdit?: boolean;
|
allowEdit?: boolean;
|
||||||
@ -40,7 +50,13 @@ export default function SalesOrderAllocationTable({
|
|||||||
const table = useTable('salesorderallocations');
|
const table = useTable('salesorderallocations');
|
||||||
|
|
||||||
const tableFilters: TableFilter[] = useMemo(() => {
|
const tableFilters: TableFilter[] = useMemo(() => {
|
||||||
return [];
|
return [
|
||||||
|
{
|
||||||
|
name: 'outstanding',
|
||||||
|
label: t`Outstanding`,
|
||||||
|
description: t`Show outstanding allocations`
|
||||||
|
}
|
||||||
|
];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const tableColumns: TableColumn[] = useMemo(() => {
|
const tableColumns: TableColumn[] = useMemo(() => {
|
||||||
@ -49,6 +65,7 @@ export default function SalesOrderAllocationTable({
|
|||||||
accessor: 'order_detail.reference',
|
accessor: 'order_detail.reference',
|
||||||
title: t`Sales Order`,
|
title: t`Sales Order`,
|
||||||
switchable: false,
|
switchable: false,
|
||||||
|
sortable: true,
|
||||||
hidden: showOrderInfo != true
|
hidden: showOrderInfo != true
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
@ -70,65 +87,138 @@ 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: 'quantity',
|
|
||||||
title: t`Allocated Quantity`,
|
|
||||||
sortable: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
accessor: 'serial',
|
accessor: 'serial',
|
||||||
title: t`Serial Number`,
|
title: t`Serial Number`,
|
||||||
sortable: false,
|
sortable: true,
|
||||||
switchable: true,
|
switchable: true,
|
||||||
render: (record: any) => record?.item_detail?.serial
|
render: (record: any) => record?.item_detail?.serial
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'batch',
|
accessor: 'batch',
|
||||||
title: t`Batch Code`,
|
title: t`Batch Code`,
|
||||||
sortable: false,
|
sortable: true,
|
||||||
switchable: true,
|
switchable: true,
|
||||||
render: (record: any) => record?.item_detail?.batch
|
render: (record: any) => record?.item_detail?.batch
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'available',
|
accessor: 'available',
|
||||||
title: t`Available Quantity`,
|
title: t`Available Quantity`,
|
||||||
|
sortable: false,
|
||||||
render: (record: any) => record?.item_detail?.quantity
|
render: (record: any) => record?.item_detail?.quantity
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessor: 'quantity',
|
||||||
|
title: t`Allocated Quantity`,
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
LocationColumn({
|
LocationColumn({
|
||||||
accessor: 'location_detail',
|
accessor: 'location_detail',
|
||||||
switchable: true,
|
switchable: true,
|
||||||
sortable: true
|
sortable: true
|
||||||
})
|
}),
|
||||||
|
{
|
||||||
|
accessor: 'shipment_detail.reference',
|
||||||
|
title: t`Shipment`,
|
||||||
|
switchable: true,
|
||||||
|
sortable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'shipment_date',
|
||||||
|
title: t`Shipped`,
|
||||||
|
switchable: true,
|
||||||
|
sortable: false,
|
||||||
|
render: (record: any) => (
|
||||||
|
<YesNoButton value={!!record.shipment_detail?.shipment_date} />
|
||||||
|
)
|
||||||
|
}
|
||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const [selectedAllocation, setSelectedAllocation] = useState<number>(0);
|
||||||
|
|
||||||
|
const editAllocationFields = useSalesOrderAllocationFields({
|
||||||
|
shipmentId: shipmentId
|
||||||
|
});
|
||||||
|
|
||||||
|
const editAllocation = useEditApiFormModal({
|
||||||
|
url: ApiEndpoints.sales_order_allocation_list,
|
||||||
|
pk: selectedAllocation,
|
||||||
|
fields: editAllocationFields,
|
||||||
|
title: t`Edit Allocation`,
|
||||||
|
table: table
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteAllocation = useDeleteApiFormModal({
|
||||||
|
url: ApiEndpoints.sales_order_allocation_list,
|
||||||
|
pk: selectedAllocation,
|
||||||
|
title: t`Delete Allocation`,
|
||||||
|
table: table
|
||||||
|
});
|
||||||
|
|
||||||
const rowActions = useCallback(
|
const rowActions = useCallback(
|
||||||
(record: any): RowAction[] => {
|
(record: any): RowAction[] => {
|
||||||
return [];
|
// Do not allow "shipped" items to be manipulated
|
||||||
|
const isShipped = !!record.shipment_detail?.shipment_date;
|
||||||
|
|
||||||
|
if (isShipped || !allowEdit) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
RowEditAction({
|
||||||
|
tooltip: t`Edit Allocation`,
|
||||||
|
onClick: () => {
|
||||||
|
setSelectedAllocation(record.pk);
|
||||||
|
editAllocation.open();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
RowDeleteAction({
|
||||||
|
tooltip: t`Delete Allocation`,
|
||||||
|
onClick: () => {
|
||||||
|
setSelectedAllocation(record.pk);
|
||||||
|
deleteAllocation.open();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
];
|
||||||
},
|
},
|
||||||
[user]
|
[allowEdit, user]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tableActions = useMemo(() => {
|
||||||
|
if (!allowEdit) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}, [allowEdit, user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvenTreeTable
|
<>
|
||||||
url={apiUrl(ApiEndpoints.sales_order_allocation_list)}
|
{editAllocation.modal}
|
||||||
tableState={table}
|
{deleteAllocation.modal}
|
||||||
columns={tableColumns}
|
<InvenTreeTable
|
||||||
props={{
|
url={apiUrl(ApiEndpoints.sales_order_allocation_list)}
|
||||||
params: {
|
tableState={table}
|
||||||
part_detail: showPartInfo ?? false,
|
columns={tableColumns}
|
||||||
order_detail: showOrderInfo ?? false,
|
props={{
|
||||||
item_detail: true,
|
params: {
|
||||||
location_detail: true,
|
part_detail: showPartInfo ?? false,
|
||||||
part: partId,
|
order_detail: showOrderInfo ?? false,
|
||||||
order: orderId,
|
item_detail: true,
|
||||||
stock_item: stockId
|
location_detail: true,
|
||||||
},
|
part: partId,
|
||||||
rowActions: rowActions,
|
order: orderId,
|
||||||
tableFilters: tableFilters,
|
shipment: shipmentId,
|
||||||
modelField: modelField ?? 'order',
|
item: stockId
|
||||||
modelType: modelTarget ?? ModelType.salesorder
|
},
|
||||||
}}
|
rowActions: rowActions,
|
||||||
/>
|
tableActions: tableActions,
|
||||||
|
tableFilters: tableFilters,
|
||||||
|
modelField: modelField ?? 'order',
|
||||||
|
modelType: modelTarget ?? ModelType.salesorder
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Text } from '@mantine/core';
|
import { Text } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
|
IconArrowRight,
|
||||||
IconHash,
|
IconHash,
|
||||||
IconShoppingCart,
|
IconShoppingCart,
|
||||||
IconSquareArrowRight,
|
IconSquareArrowRight,
|
||||||
@ -8,6 +9,7 @@ import {
|
|||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { ActionButton } from '../../components/buttons/ActionButton';
|
||||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||||
import { ProgressBar } from '../../components/items/ProgressBar';
|
import { ProgressBar } from '../../components/items/ProgressBar';
|
||||||
import { formatCurrency } from '../../defaults/formatters';
|
import { formatCurrency } from '../../defaults/formatters';
|
||||||
@ -16,6 +18,7 @@ import { ModelType } from '../../enums/ModelType';
|
|||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { useBuildOrderFields } from '../../forms/BuildForms';
|
import { useBuildOrderFields } from '../../forms/BuildForms';
|
||||||
import {
|
import {
|
||||||
|
useAllocateToSalesOrderForm,
|
||||||
useSalesOrderAllocateSerialsFields,
|
useSalesOrderAllocateSerialsFields,
|
||||||
useSalesOrderLineItemFields
|
useSalesOrderLineItemFields
|
||||||
} from '../../forms/SalesOrderForms';
|
} from '../../forms/SalesOrderForms';
|
||||||
@ -236,6 +239,7 @@ export default function SalesOrderLineItemTable({
|
|||||||
url: ApiEndpoints.sales_order_allocate_serials,
|
url: ApiEndpoints.sales_order_allocate_serials,
|
||||||
pk: orderId,
|
pk: orderId,
|
||||||
title: t`Allocate Serial Numbers`,
|
title: t`Allocate Serial Numbers`,
|
||||||
|
initialData: initialData,
|
||||||
fields: allocateSerialFields,
|
fields: allocateSerialFields,
|
||||||
table: table
|
table: table
|
||||||
});
|
});
|
||||||
@ -251,6 +255,17 @@ export default function SalesOrderLineItemTable({
|
|||||||
modelType: ModelType.build
|
modelType: ModelType.build
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [selectedItems, setSelectedItems] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const allocateStock = useAllocateToSalesOrderForm({
|
||||||
|
orderId: orderId,
|
||||||
|
lineItems: selectedItems,
|
||||||
|
onFormSuccess: () => {
|
||||||
|
table.refreshTable();
|
||||||
|
table.clearSelectedRecords();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const tableActions = useMemo(() => {
|
const tableActions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
<AddItemButton
|
<AddItemButton
|
||||||
@ -263,9 +278,22 @@ export default function SalesOrderLineItemTable({
|
|||||||
newLine.open();
|
newLine.open();
|
||||||
}}
|
}}
|
||||||
hidden={!editable || !user.hasAddRole(UserRoles.sales_order)}
|
hidden={!editable || !user.hasAddRole(UserRoles.sales_order)}
|
||||||
|
/>,
|
||||||
|
<ActionButton
|
||||||
|
key="allocate-stock"
|
||||||
|
tooltip={t`Allocate Stock`}
|
||||||
|
icon={<IconArrowRight />}
|
||||||
|
disabled={!table.hasSelectedRecords}
|
||||||
|
color="green"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedItems(
|
||||||
|
table.selectedRecords.filter((r) => r.allocated < r.quantity)
|
||||||
|
);
|
||||||
|
allocateStock.open();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
];
|
];
|
||||||
}, [user, orderId]);
|
}, [user, orderId, table.hasSelectedRecords, table.selectedRecords]);
|
||||||
|
|
||||||
const rowActions = useCallback(
|
const rowActions = useCallback(
|
||||||
(record: any): RowAction[] => {
|
(record: any): RowAction[] => {
|
||||||
@ -280,7 +308,10 @@ export default function SalesOrderLineItemTable({
|
|||||||
title: t`Allocate Stock`,
|
title: t`Allocate Stock`,
|
||||||
icon: <IconSquareArrowRight />,
|
icon: <IconSquareArrowRight />,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
onClick: notYetImplemented
|
onClick: () => {
|
||||||
|
setSelectedItems([record]);
|
||||||
|
allocateStock.open();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
hidden:
|
hidden:
|
||||||
@ -288,11 +319,14 @@ export default function SalesOrderLineItemTable({
|
|||||||
allocated ||
|
allocated ||
|
||||||
!editable ||
|
!editable ||
|
||||||
!user.hasChangeRole(UserRoles.sales_order),
|
!user.hasChangeRole(UserRoles.sales_order),
|
||||||
title: t`Allocate Serials`,
|
title: t`Allocate serials`,
|
||||||
icon: <IconHash />,
|
icon: <IconHash />,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setSelectedLine(record.pk);
|
setSelectedLine(record.pk);
|
||||||
|
setInitialData({
|
||||||
|
quantity: record.quantity - record.allocated
|
||||||
|
});
|
||||||
allocateBySerials.open();
|
allocateBySerials.open();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -356,6 +390,7 @@ export default function SalesOrderLineItemTable({
|
|||||||
{newLine.modal}
|
{newLine.modal}
|
||||||
{newBuildOrder.modal}
|
{newBuildOrder.modal}
|
||||||
{allocateBySerials.modal}
|
{allocateBySerials.modal}
|
||||||
|
{allocateStock.modal}
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url={apiUrl(ApiEndpoints.sales_order_line_list)}
|
url={apiUrl(ApiEndpoints.sales_order_line_list)}
|
||||||
tableState={table}
|
tableState={table}
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { IconTruckDelivery } from '@tabler/icons-react';
|
import { IconArrowRight, IconTruckDelivery } from '@tabler/icons-react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
import { useSalesOrderShipmentFields } from '../../forms/SalesOrderForms';
|
import {
|
||||||
|
useSalesOrderShipmentCompleteFields,
|
||||||
|
useSalesOrderShipmentFields
|
||||||
|
} from '../../forms/SalesOrderForms';
|
||||||
|
import { navigateToLink } from '../../functions/navigation';
|
||||||
import { notYetImplemented } from '../../functions/notifications';
|
import { notYetImplemented } from '../../functions/notifications';
|
||||||
|
import { getDetailUrl } from '../../functions/urls';
|
||||||
import {
|
import {
|
||||||
useCreateApiFormModal,
|
useCreateApiFormModal,
|
||||||
useDeleteApiFormModal,
|
useDeleteApiFormModal,
|
||||||
@ -20,7 +26,7 @@ import { TableColumn } from '../Column';
|
|||||||
import { DateColumn, LinkColumn, NoteColumn } from '../ColumnRenderers';
|
import { DateColumn, LinkColumn, NoteColumn } from '../ColumnRenderers';
|
||||||
import { TableFilter } from '../Filter';
|
import { TableFilter } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
|
import { RowAction, RowCancelAction, RowEditAction } from '../RowActions';
|
||||||
|
|
||||||
export default function SalesOrderShipmentTable({
|
export default function SalesOrderShipmentTable({
|
||||||
orderId
|
orderId
|
||||||
@ -28,12 +34,16 @@ export default function SalesOrderShipmentTable({
|
|||||||
orderId: number;
|
orderId: number;
|
||||||
}>) {
|
}>) {
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
|
const navigate = useNavigate();
|
||||||
const table = useTable('sales-order-shipment');
|
const table = useTable('sales-order-shipment');
|
||||||
|
|
||||||
const [selectedShipment, setSelectedShipment] = useState<number>(0);
|
const [selectedShipment, setSelectedShipment] = useState<any>({});
|
||||||
|
|
||||||
const newShipmentFields = useSalesOrderShipmentFields();
|
const newShipmentFields = useSalesOrderShipmentFields({});
|
||||||
const editShipmentFields = useSalesOrderShipmentFields();
|
|
||||||
|
const editShipmentFields = useSalesOrderShipmentFields({});
|
||||||
|
|
||||||
|
const completeShipmentFields = useSalesOrderShipmentCompleteFields({});
|
||||||
|
|
||||||
const newShipment = useCreateApiFormModal({
|
const newShipment = useCreateApiFormModal({
|
||||||
url: ApiEndpoints.sales_order_shipment_list,
|
url: ApiEndpoints.sales_order_shipment_list,
|
||||||
@ -47,33 +57,45 @@ export default function SalesOrderShipmentTable({
|
|||||||
|
|
||||||
const deleteShipment = useDeleteApiFormModal({
|
const deleteShipment = useDeleteApiFormModal({
|
||||||
url: ApiEndpoints.sales_order_shipment_list,
|
url: ApiEndpoints.sales_order_shipment_list,
|
||||||
pk: selectedShipment,
|
pk: selectedShipment.pk,
|
||||||
title: t`Delete Shipment`,
|
title: t`Cancel Shipment`,
|
||||||
table: table
|
table: table
|
||||||
});
|
});
|
||||||
|
|
||||||
const editShipment = useEditApiFormModal({
|
const editShipment = useEditApiFormModal({
|
||||||
url: ApiEndpoints.sales_order_shipment_list,
|
url: ApiEndpoints.sales_order_shipment_list,
|
||||||
pk: selectedShipment,
|
pk: selectedShipment.pk,
|
||||||
fields: editShipmentFields,
|
fields: editShipmentFields,
|
||||||
title: t`Edit Shipment`,
|
title: t`Edit Shipment`,
|
||||||
table: table
|
table: table
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const completeShipment = useCreateApiFormModal({
|
||||||
|
url: ApiEndpoints.sales_order_shipment_complete,
|
||||||
|
pk: selectedShipment.pk,
|
||||||
|
fields: completeShipmentFields,
|
||||||
|
title: t`Complete Shipment`,
|
||||||
|
table: table,
|
||||||
|
focus: 'tracking_number',
|
||||||
|
initialData: {
|
||||||
|
...selectedShipment,
|
||||||
|
shipment_date: new Date().toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const tableColumns: TableColumn[] = useMemo(() => {
|
const tableColumns: TableColumn[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
accessor: 'reference',
|
accessor: 'reference',
|
||||||
title: t`Shipment Reference`,
|
title: t`Shipment Reference`,
|
||||||
switchable: false
|
switchable: false,
|
||||||
|
sortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessor: 'allocations',
|
accessor: 'allocated_items',
|
||||||
title: t`Items`,
|
sortable: true,
|
||||||
render: (record: any) => {
|
switchable: false,
|
||||||
let allocations = record?.allocations ?? [];
|
title: t`Items`
|
||||||
return allocations.length;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
DateColumn({
|
DateColumn({
|
||||||
accessor: 'shipment_date',
|
accessor: 'shipment_date',
|
||||||
@ -91,9 +113,6 @@ export default function SalesOrderShipmentTable({
|
|||||||
},
|
},
|
||||||
LinkColumn({
|
LinkColumn({
|
||||||
accessor: 'link'
|
accessor: 'link'
|
||||||
}),
|
|
||||||
NoteColumn({
|
|
||||||
accessor: 'notes'
|
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
}, []);
|
}, []);
|
||||||
@ -103,23 +122,40 @@ export default function SalesOrderShipmentTable({
|
|||||||
const shipped: boolean = !!record.shipment_date;
|
const shipped: boolean = !!record.shipment_date;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
title: t`View Shipment`,
|
||||||
|
icon: <IconArrowRight />,
|
||||||
|
onClick: (event: any) => {
|
||||||
|
navigateToLink(
|
||||||
|
getDetailUrl(ModelType.salesordershipment, record.pk),
|
||||||
|
navigate,
|
||||||
|
event
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
hidden: shipped || !user.hasChangeRole(UserRoles.sales_order),
|
hidden: shipped || !user.hasChangeRole(UserRoles.sales_order),
|
||||||
title: t`Complete Shipment`,
|
title: t`Complete Shipment`,
|
||||||
|
color: 'green',
|
||||||
icon: <IconTruckDelivery />,
|
icon: <IconTruckDelivery />,
|
||||||
onClick: notYetImplemented
|
onClick: () => {
|
||||||
|
setSelectedShipment(record);
|
||||||
|
completeShipment.open();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
RowEditAction({
|
RowEditAction({
|
||||||
hidden: !user.hasChangeRole(UserRoles.sales_order),
|
hidden: !user.hasChangeRole(UserRoles.sales_order),
|
||||||
|
tooltip: t`Edit shipment`,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setSelectedShipment(record.pk);
|
setSelectedShipment(record);
|
||||||
editShipment.open();
|
editShipment.open();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
RowDeleteAction({
|
RowCancelAction({
|
||||||
hidden: !user.hasDeleteRole(UserRoles.sales_order),
|
hidden: shipped || !user.hasDeleteRole(UserRoles.sales_order),
|
||||||
|
tooltip: t`Cancel shipment`,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setSelectedShipment(record.pk);
|
setSelectedShipment(record);
|
||||||
deleteShipment.open();
|
deleteShipment.open();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -161,6 +197,7 @@ export default function SalesOrderShipmentTable({
|
|||||||
{newShipment.modal}
|
{newShipment.modal}
|
||||||
{editShipment.modal}
|
{editShipment.modal}
|
||||||
{deleteShipment.modal}
|
{deleteShipment.modal}
|
||||||
|
{completeShipment.modal}
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url={apiUrl(ApiEndpoints.sales_order_shipment_list)}
|
url={apiUrl(ApiEndpoints.sales_order_shipment_list)}
|
||||||
tableState={table}
|
tableState={table}
|
||||||
|
@ -41,6 +41,92 @@ test('Sales Orders', async ({ page }) => {
|
|||||||
await page.getByRole('button', { name: 'Issue Order' }).waitFor();
|
await page.getByRole('button', { name: 'Issue Order' }).waitFor();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Sales Orders - Shipments', async ({ page }) => {
|
||||||
|
await doQuickLogin(page);
|
||||||
|
|
||||||
|
await page.goto(`${baseUrl}/home`);
|
||||||
|
await page.getByRole('tab', { name: 'Sales' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Sales Orders' }).click();
|
||||||
|
|
||||||
|
// Click through to a particular sales order
|
||||||
|
await page.getByRole('tab', { name: 'Sales Orders' }).waitFor();
|
||||||
|
await page.getByRole('cell', { name: 'SO0006' }).first().click();
|
||||||
|
await page.getByRole('tab', { name: 'Shipments' }).click();
|
||||||
|
|
||||||
|
// Create a new shipment
|
||||||
|
await page.getByLabel('action-button-add-shipment').click();
|
||||||
|
await page.getByLabel('text-field-tracking_number').fill('1234567890');
|
||||||
|
await page.getByLabel('text-field-invoice_number').fill('9876543210');
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
// Expected field error
|
||||||
|
await page
|
||||||
|
.getByText('The fields order, reference must make a unique set')
|
||||||
|
.first()
|
||||||
|
.waitFor();
|
||||||
|
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||||
|
|
||||||
|
// Edit one of the existing shipments
|
||||||
|
await page.getByLabel('row-action-menu-0').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||||
|
|
||||||
|
// Ensure the form has loaded
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
let tracking_number = await page
|
||||||
|
.getByLabel('text-field-tracking_number')
|
||||||
|
.inputValue();
|
||||||
|
|
||||||
|
if (!tracking_number) {
|
||||||
|
tracking_number = '1234567890';
|
||||||
|
} else if (tracking_number.endsWith('x')) {
|
||||||
|
// Remove the 'x' from the end of the tracking number
|
||||||
|
tracking_number = tracking_number.substring(0, tracking_number.length - 1);
|
||||||
|
} else {
|
||||||
|
// Add an 'x' to the end of the tracking number
|
||||||
|
tracking_number += 'x';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the tracking number
|
||||||
|
await page.getByLabel('text-field-tracking_number').fill(tracking_number);
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
|
||||||
|
// Click through to a particular shipment
|
||||||
|
await page.getByLabel('row-action-menu-0').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'View Shipment' }).click();
|
||||||
|
|
||||||
|
// Click through the various tabs
|
||||||
|
await page.getByRole('tab', { name: 'Attachments' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Notes' }).click();
|
||||||
|
await page.getByRole('tab', { name: 'Assigned Items' }).click();
|
||||||
|
|
||||||
|
// Ensure assigned items table loads correctly
|
||||||
|
await page.getByRole('cell', { name: 'BATCH-001' }).first().waitFor();
|
||||||
|
|
||||||
|
await page.getByRole('tab', { name: 'Shipment Details' }).click();
|
||||||
|
|
||||||
|
// The "new" tracking number should be visible
|
||||||
|
await page.getByText(tracking_number).waitFor();
|
||||||
|
|
||||||
|
// Link back to sales order
|
||||||
|
await page.getByRole('link', { name: 'SO0006' }).click();
|
||||||
|
|
||||||
|
// Let's try to allocate some stock
|
||||||
|
await page.getByRole('tab', { name: 'Line Items' }).click();
|
||||||
|
await page.getByLabel('row-action-menu-1').click();
|
||||||
|
await page.getByRole('menuitem', { name: 'Allocate stock' }).click();
|
||||||
|
await page
|
||||||
|
.getByText('Select the source location for the stock allocation')
|
||||||
|
.waitFor();
|
||||||
|
await page.getByLabel('number-field-quantity').fill('123');
|
||||||
|
await page.getByLabel('related-field-stock_item').click();
|
||||||
|
await page.getByText('Quantity: 42').click();
|
||||||
|
await page.getByRole('button', { name: 'Submit' }).click();
|
||||||
|
await page.getByText('This field is required.').waitFor();
|
||||||
|
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||||
|
});
|
||||||
|
|
||||||
test('Purchase Orders', async ({ page }) => {
|
test('Purchase Orders', async ({ page }) => {
|
||||||
await doQuickLogin(page);
|
await doQuickLogin(page);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user