mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	[PUI] pricing tab (#6985)
* Add recharts package - Brings us in-line with mantine v7 * Add skeleton pricing page * Fetch pricing data * Rough implementation of variant pricing chart - Needs better labels for tooltip and axis * Cleanup * More cleanup * Improve rendering * Add pricing overview * Add pie chart for BOM pricing - Needs extra work! * Split components into separate files * Backend: allow ordering parts by pricing * Bump API version * Update VariantPricingPanel: - Table drives data selection now * Refactor BomPricingPanel - Table drives the data * Allow BomItemList to be sorted by pricing too * Sort bom table * Make record index available to render function * Refactor BomPricingPanel - Better rendering of pie chart * Update pricing overview panel * Further updates - Expose "pricing_updated" column to API endpoints - Allow ordering by "pricing_updated" column * Update API endpoint for PurchaseOrderLineItem * Implement PurchaseOrderHistory panel * Cleanup PurchaseHistoryPanel * Enhance API for SupplierPriceBreak * Implement SupplierPricingPanel * Fix for getDetailUrl - Take base URL into account also! * Further fixes for getDetailUrl * Fix number form field * Implement SupplierPriceBreakTable * Tweaks for StockItemTable * Ensure frontend is translated when compiling static files * Fixes for BomPricingPanel * Simplify price rendering for bom table * Update BomItem serializer - Add pricing_min_total - Add pricing_max_total - Fix existing 1+N query issue * Use values provided by API * Fix BomItem serializer lookup * Refactor pricing charts * Fix for VariantPricingPanel * Remove unused imports * Implement SalePriceBreak table - Refactor the InternalPriceBreak table to be generic * Allow price breaks to be ordered by 'price' * Display alert for no available data * Update backend API filters * Allow ordering by customer * Implement SaleHistoryPanel * Allow user to select pie or bar chart for BOM pricing detail * Remove extra padding
This commit is contained in:
		| @@ -1,11 +1,17 @@ | |||||||
| """InvenTree API version information.""" | """InvenTree API version information.""" | ||||||
|  |  | ||||||
| # InvenTree API version | # InvenTree API version | ||||||
| INVENTREE_API_VERSION = 186 | INVENTREE_API_VERSION = 187 | ||||||
| """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 = """ | ||||||
|  |  | ||||||
|  | v187 - 2024-03-10 : https://github.com/inventree/InvenTree/pull/6985 | ||||||
|  |     - Allow Part list endpoint to be sorted by pricing_min and pricing_max values | ||||||
|  |     - Allow BomItem list endpoint to be sorted by pricing_min and pricing_max values | ||||||
|  |     - Allow InternalPrice and SalePrice endpoints to be sorted by quantity | ||||||
|  |     - Adds total pricing values to BomItem serializer | ||||||
|  |  | ||||||
| v186 - 2024-03-26 : https://github.com/inventree/InvenTree/pull/6855 | v186 - 2024-03-26 : https://github.com/inventree/InvenTree/pull/6855 | ||||||
|     - Adds license information to the API |     - Adds license information to the API | ||||||
|  |  | ||||||
|   | |||||||
| @@ -468,9 +468,13 @@ class SupplierPriceBreakList(ListCreateAPI): | |||||||
|  |  | ||||||
|         return self.serializer_class(*args, **kwargs) |         return self.serializer_class(*args, **kwargs) | ||||||
|  |  | ||||||
|     filter_backends = ORDER_FILTER |     filter_backends = SEARCH_ORDER_FILTER_ALIAS | ||||||
|  |  | ||||||
|     ordering_fields = ['quantity'] |     ordering_fields = ['quantity', 'supplier', 'SKU', 'price'] | ||||||
|  |  | ||||||
|  |     search_fields = ['part__SKU', 'part__supplier__name'] | ||||||
|  |  | ||||||
|  |     ordering_field_aliases = {'supplier': 'part__supplier__name', 'SKU': 'part__SKU'} | ||||||
|  |  | ||||||
|     ordering = 'quantity' |     ordering = 'quantity' | ||||||
|  |  | ||||||
|   | |||||||
| @@ -154,11 +154,11 @@ class LineItemFilter(rest_filters.FilterSet): | |||||||
|  |  | ||||||
|     # Filter by order status |     # Filter by order status | ||||||
|     order_status = rest_filters.NumberFilter( |     order_status = rest_filters.NumberFilter( | ||||||
|         label='order_status', field_name='order__status' |         label=_('Order Status'), field_name='order__status' | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     has_pricing = rest_filters.BooleanFilter( |     has_pricing = rest_filters.BooleanFilter( | ||||||
|         label='Has Pricing', method='filter_has_pricing' |         label=_('Has Pricing'), method='filter_has_pricing' | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     def filter_has_pricing(self, queryset, name, value): |     def filter_has_pricing(self, queryset, name, value): | ||||||
| @@ -425,9 +425,38 @@ class PurchaseOrderLineItemFilter(LineItemFilter): | |||||||
|  |  | ||||||
|         price_field = 'purchase_price' |         price_field = 'purchase_price' | ||||||
|         model = models.PurchaseOrderLineItem |         model = models.PurchaseOrderLineItem | ||||||
|         fields = ['order', 'part'] |         fields = [] | ||||||
|  |  | ||||||
|     pending = rest_filters.BooleanFilter(label='pending', method='filter_pending') |     order = rest_filters.ModelChoiceFilter( | ||||||
|  |         queryset=models.PurchaseOrder.objects.all(), | ||||||
|  |         field_name='order', | ||||||
|  |         label=_('Order'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     order_complete = rest_filters.BooleanFilter( | ||||||
|  |         label=_('Order Complete'), method='filter_order_complete' | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     def filter_order_complete(self, queryset, name, value): | ||||||
|  |         """Filter by whether the order is 'complete' or not.""" | ||||||
|  |         if str2bool(value): | ||||||
|  |             return queryset.filter(order__status=PurchaseOrderStatus.COMPLETE.value) | ||||||
|  |  | ||||||
|  |         return queryset.exclude(order__status=PurchaseOrderStatus.COMPLETE.value) | ||||||
|  |  | ||||||
|  |     part = rest_filters.ModelChoiceFilter( | ||||||
|  |         queryset=SupplierPart.objects.all(), field_name='part', label=_('Supplier Part') | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     base_part = rest_filters.ModelChoiceFilter( | ||||||
|  |         queryset=Part.objects.filter(purchaseable=True), | ||||||
|  |         field_name='part__part', | ||||||
|  |         label=_('Internal Part'), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     pending = rest_filters.BooleanFilter( | ||||||
|  |         method='filter_pending', label=_('Order Pending') | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     def filter_pending(self, queryset, name, value): |     def filter_pending(self, queryset, name, value): | ||||||
|         """Filter by "pending" status (order status = pending).""" |         """Filter by "pending" status (order status = pending).""" | ||||||
| @@ -435,7 +464,9 @@ class PurchaseOrderLineItemFilter(LineItemFilter): | |||||||
|             return queryset.filter(order__status__in=PurchaseOrderStatusGroups.OPEN) |             return queryset.filter(order__status__in=PurchaseOrderStatusGroups.OPEN) | ||||||
|         return queryset.exclude(order__status__in=PurchaseOrderStatusGroups.OPEN) |         return queryset.exclude(order__status__in=PurchaseOrderStatusGroups.OPEN) | ||||||
|  |  | ||||||
|     received = rest_filters.BooleanFilter(label='received', method='filter_received') |     received = rest_filters.BooleanFilter( | ||||||
|  |         label=_('Items Received'), method='filter_received' | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     def filter_received(self, queryset, name, value): |     def filter_received(self, queryset, name, value): | ||||||
|         """Filter by lines which are "received" (or "not" received). |         """Filter by lines which are "received" (or "not" received). | ||||||
| @@ -542,25 +573,6 @@ class PurchaseOrderLineItemList( | |||||||
|             serializer.data, status=status.HTTP_201_CREATED, headers=headers |             serializer.data, status=status.HTTP_201_CREATED, headers=headers | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def filter_queryset(self, queryset): |  | ||||||
|         """Additional filtering options.""" |  | ||||||
|         params = self.request.query_params |  | ||||||
|  |  | ||||||
|         queryset = super().filter_queryset(queryset) |  | ||||||
|  |  | ||||||
|         base_part = params.get('base_part', None) |  | ||||||
|  |  | ||||||
|         if base_part: |  | ||||||
|             try: |  | ||||||
|                 base_part = Part.objects.get(pk=base_part) |  | ||||||
|  |  | ||||||
|                 queryset = queryset.filter(part__part=base_part) |  | ||||||
|  |  | ||||||
|             except (ValueError, Part.DoesNotExist): |  | ||||||
|                 pass |  | ||||||
|  |  | ||||||
|         return queryset |  | ||||||
|  |  | ||||||
|     def download_queryset(self, queryset, export_format): |     def download_queryset(self, queryset, export_format): | ||||||
|         """Download the requested queryset as a file.""" |         """Download the requested queryset as a file.""" | ||||||
|         dataset = PurchaseOrderLineItemResource().export(queryset=queryset) |         dataset = PurchaseOrderLineItemResource().export(queryset=queryset) | ||||||
| @@ -577,6 +589,8 @@ class PurchaseOrderLineItemList( | |||||||
|         'MPN': 'part__manufacturer_part__MPN', |         'MPN': 'part__manufacturer_part__MPN', | ||||||
|         'SKU': 'part__SKU', |         'SKU': 'part__SKU', | ||||||
|         'part_name': 'part__part__name', |         'part_name': 'part__part__name', | ||||||
|  |         'order': 'order__reference', | ||||||
|  |         'complete_date': 'order__complete_date', | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     ordering_fields = [ |     ordering_fields = [ | ||||||
| @@ -589,6 +603,8 @@ class PurchaseOrderLineItemList( | |||||||
|         'SKU', |         'SKU', | ||||||
|         'total_price', |         'total_price', | ||||||
|         'target_date', |         'target_date', | ||||||
|  |         'order', | ||||||
|  |         'complete_date', | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     search_fields = [ |     search_fields = [ | ||||||
| @@ -791,7 +807,15 @@ class SalesOrderLineItemFilter(LineItemFilter): | |||||||
|  |  | ||||||
|         price_field = 'sale_price' |         price_field = 'sale_price' | ||||||
|         model = models.SalesOrderLineItem |         model = models.SalesOrderLineItem | ||||||
|         fields = ['order', 'part'] |         fields = [] | ||||||
|  |  | ||||||
|  |     order = rest_filters.ModelChoiceFilter( | ||||||
|  |         queryset=models.SalesOrder.objects.all(), field_name='order', label=_('Order') | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     part = rest_filters.ModelChoiceFilter( | ||||||
|  |         queryset=Part.objects.all(), field_name='part', label=_('Part') | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     completed = rest_filters.BooleanFilter(label='completed', method='filter_completed') |     completed = rest_filters.BooleanFilter(label='completed', method='filter_completed') | ||||||
|  |  | ||||||
| @@ -806,6 +830,17 @@ class SalesOrderLineItemFilter(LineItemFilter): | |||||||
|             return queryset.filter(q) |             return queryset.filter(q) | ||||||
|         return queryset.exclude(q) |         return queryset.exclude(q) | ||||||
|  |  | ||||||
|  |     order_complete = rest_filters.BooleanFilter( | ||||||
|  |         label=_('Order Complete'), method='filter_order_complete' | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     def filter_order_complete(self, queryset, name, value): | ||||||
|  |         """Filter by whether the order is 'complete' or not.""" | ||||||
|  |         if str2bool(value): | ||||||
|  |             return queryset.filter(order__status__in=SalesOrderStatusGroups.COMPLETE) | ||||||
|  |  | ||||||
|  |         return queryset.exclude(order__status__in=SalesOrderStatusGroups.COMPLETE) | ||||||
|  |  | ||||||
|  |  | ||||||
| class SalesOrderLineItemMixin: | class SalesOrderLineItemMixin: | ||||||
|     """Mixin class for SalesOrderLineItem endpoints.""" |     """Mixin class for SalesOrderLineItem endpoints.""" | ||||||
| @@ -862,9 +897,24 @@ class SalesOrderLineItemList(SalesOrderLineItemMixin, APIDownloadMixin, ListCrea | |||||||
|  |  | ||||||
|         return DownloadFile(filedata, filename) |         return DownloadFile(filedata, filename) | ||||||
|  |  | ||||||
|     filter_backends = SEARCH_ORDER_FILTER |     filter_backends = SEARCH_ORDER_FILTER_ALIAS | ||||||
|  |  | ||||||
|     ordering_fields = ['part__name', 'quantity', 'reference', 'target_date'] |     ordering_fields = [ | ||||||
|  |         'customer', | ||||||
|  |         'order', | ||||||
|  |         'part', | ||||||
|  |         'part__name', | ||||||
|  |         'quantity', | ||||||
|  |         'reference', | ||||||
|  |         'sale_price', | ||||||
|  |         'target_date', | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     ordering_field_aliases = { | ||||||
|  |         'customer': 'order__customer__name', | ||||||
|  |         'part': 'part__name', | ||||||
|  |         'order': 'order__reference', | ||||||
|  |     } | ||||||
|  |  | ||||||
|     search_fields = ['part__name', 'quantity', 'reference'] |     search_fields = ['part__name', 'quantity', 'reference'] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -38,7 +38,6 @@ from InvenTree.helpers import ( | |||||||
|     is_ajax, |     is_ajax, | ||||||
|     isNull, |     isNull, | ||||||
|     str2bool, |     str2bool, | ||||||
|     str2int, |  | ||||||
| ) | ) | ||||||
| from InvenTree.mixins import ( | from InvenTree.mixins import ( | ||||||
|     CreateAPI, |     CreateAPI, | ||||||
| @@ -386,9 +385,10 @@ class PartSalePriceList(ListCreateAPI): | |||||||
|     queryset = PartSellPriceBreak.objects.all() |     queryset = PartSellPriceBreak.objects.all() | ||||||
|     serializer_class = part_serializers.PartSalePriceSerializer |     serializer_class = part_serializers.PartSalePriceSerializer | ||||||
|  |  | ||||||
|     filter_backends = [DjangoFilterBackend] |     filter_backends = SEARCH_ORDER_FILTER | ||||||
|  |  | ||||||
|     filterset_fields = ['part'] |     filterset_fields = ['part'] | ||||||
|  |     ordering_fields = ['quantity', 'price'] | ||||||
|  |     ordering = 'quantity' | ||||||
|  |  | ||||||
|  |  | ||||||
| class PartInternalPriceDetail(RetrieveUpdateDestroyAPI): | class PartInternalPriceDetail(RetrieveUpdateDestroyAPI): | ||||||
| @@ -405,9 +405,10 @@ class PartInternalPriceList(ListCreateAPI): | |||||||
|     serializer_class = part_serializers.PartInternalPriceSerializer |     serializer_class = part_serializers.PartInternalPriceSerializer | ||||||
|     permission_required = 'roles.sales_order.show' |     permission_required = 'roles.sales_order.show' | ||||||
|  |  | ||||||
|     filter_backends = [DjangoFilterBackend] |     filter_backends = SEARCH_ORDER_FILTER | ||||||
|  |  | ||||||
|     filterset_fields = ['part'] |     filterset_fields = ['part'] | ||||||
|  |     ordering_fields = ['quantity', 'price'] | ||||||
|  |     ordering = 'quantity' | ||||||
|  |  | ||||||
|  |  | ||||||
| class PartAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): | class PartAttachmentList(AttachmentMixin, ListCreateDestroyAPIView): | ||||||
| @@ -1407,8 +1408,17 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI): | |||||||
|         'category', |         'category', | ||||||
|         'last_stocktake', |         'last_stocktake', | ||||||
|         'units', |         'units', | ||||||
|  |         'pricing_min', | ||||||
|  |         'pricing_max', | ||||||
|  |         'pricing_updated', | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |     ordering_field_aliases = { | ||||||
|  |         'pricing_min': 'pricing_data__overall_min', | ||||||
|  |         'pricing_max': 'pricing_data__overall_max', | ||||||
|  |         'pricing_updated': 'pricing_data__updated', | ||||||
|  |     } | ||||||
|  |  | ||||||
|     # Default ordering |     # Default ordering | ||||||
|     ordering = 'name' |     ordering = 'name' | ||||||
|  |  | ||||||
| @@ -1939,9 +1949,19 @@ class BomList(BomMixin, ListCreateDestroyAPIView): | |||||||
|         'inherited', |         'inherited', | ||||||
|         'optional', |         'optional', | ||||||
|         'consumable', |         'consumable', | ||||||
|  |         'pricing_min', | ||||||
|  |         'pricing_max', | ||||||
|  |         'pricing_min_total', | ||||||
|  |         'pricing_max_total', | ||||||
|  |         'pricing_updated', | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     ordering_field_aliases = {'sub_part': 'sub_part__name'} |     ordering_field_aliases = { | ||||||
|  |         'sub_part': 'sub_part__name', | ||||||
|  |         'pricing_min': 'sub_part__pricing_data__overall_min', | ||||||
|  |         'pricing_max': 'sub_part__pricing_data__overall_max', | ||||||
|  |         'pricing_updated': 'sub_part__pricing_data__updated', | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| class BomDetail(BomMixin, RetrieveUpdateDestroyAPI): | class BomDetail(BomMixin, RetrieveUpdateDestroyAPI): | ||||||
|   | |||||||
| @@ -616,6 +616,7 @@ class PartSerializer( | |||||||
|             'virtual', |             'virtual', | ||||||
|             'pricing_min', |             'pricing_min', | ||||||
|             'pricing_max', |             'pricing_max', | ||||||
|  |             'pricing_updated', | ||||||
|             'responsible', |             'responsible', | ||||||
|             # Annotated fields |             # Annotated fields | ||||||
|             'allocated_to_build_orders', |             'allocated_to_build_orders', | ||||||
| @@ -678,6 +679,7 @@ class PartSerializer( | |||||||
|         if not pricing: |         if not pricing: | ||||||
|             self.fields.pop('pricing_min') |             self.fields.pop('pricing_min') | ||||||
|             self.fields.pop('pricing_max') |             self.fields.pop('pricing_max') | ||||||
|  |             self.fields.pop('pricing_updated') | ||||||
|  |  | ||||||
|     def get_api_url(self): |     def get_api_url(self): | ||||||
|         """Return the API url associated with this serializer.""" |         """Return the API url associated with this serializer.""" | ||||||
| @@ -843,6 +845,9 @@ class PartSerializer( | |||||||
|     pricing_max = InvenTree.serializers.InvenTreeMoneySerializer( |     pricing_max = InvenTree.serializers.InvenTreeMoneySerializer( | ||||||
|         source='pricing_data.overall_max', allow_null=True, read_only=True |         source='pricing_data.overall_max', allow_null=True, read_only=True | ||||||
|     ) |     ) | ||||||
|  |     pricing_updated = serializers.DateTimeField( | ||||||
|  |         source='pricing_data.updated', allow_null=True, read_only=True | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     parameters = PartParameterSerializer(many=True, read_only=True) |     parameters = PartParameterSerializer(many=True, read_only=True) | ||||||
|  |  | ||||||
| @@ -1413,6 +1418,9 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer): | |||||||
|             'part_detail', |             'part_detail', | ||||||
|             'pricing_min', |             'pricing_min', | ||||||
|             'pricing_max', |             'pricing_max', | ||||||
|  |             'pricing_min_total', | ||||||
|  |             'pricing_max_total', | ||||||
|  |             'pricing_updated', | ||||||
|             'quantity', |             'quantity', | ||||||
|             'reference', |             'reference', | ||||||
|             'sub_part', |             'sub_part', | ||||||
| @@ -1451,6 +1459,9 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer): | |||||||
|         if not pricing: |         if not pricing: | ||||||
|             self.fields.pop('pricing_min') |             self.fields.pop('pricing_min') | ||||||
|             self.fields.pop('pricing_max') |             self.fields.pop('pricing_max') | ||||||
|  |             self.fields.pop('pricing_min_total') | ||||||
|  |             self.fields.pop('pricing_max_total') | ||||||
|  |             self.fields.pop('pricing_updated') | ||||||
|  |  | ||||||
|     quantity = InvenTree.serializers.InvenTreeDecimalField(required=True) |     quantity = InvenTree.serializers.InvenTreeDecimalField(required=True) | ||||||
|  |  | ||||||
| @@ -1481,10 +1492,22 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer): | |||||||
|  |  | ||||||
|     # Cached pricing fields |     # Cached pricing fields | ||||||
|     pricing_min = InvenTree.serializers.InvenTreeMoneySerializer( |     pricing_min = InvenTree.serializers.InvenTreeMoneySerializer( | ||||||
|         source='sub_part.pricing.overall_min', allow_null=True, read_only=True |         source='sub_part.pricing_data.overall_min', allow_null=True, read_only=True | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     pricing_max = InvenTree.serializers.InvenTreeMoneySerializer( |     pricing_max = InvenTree.serializers.InvenTreeMoneySerializer( | ||||||
|         source='sub_part.pricing.overall_max', allow_null=True, read_only=True |         source='sub_part.pricing_data.overall_max', allow_null=True, read_only=True | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     pricing_min_total = InvenTree.serializers.InvenTreeMoneySerializer( | ||||||
|  |         allow_null=True, read_only=True | ||||||
|  |     ) | ||||||
|  |     pricing_max_total = InvenTree.serializers.InvenTreeMoneySerializer( | ||||||
|  |         allow_null=True, read_only=True | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     pricing_updated = serializers.DateTimeField( | ||||||
|  |         source='sub_part.pricing_data.updated', allow_null=True, read_only=True | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     # Annotated fields for available stock |     # Annotated fields for available stock | ||||||
| @@ -1504,6 +1527,7 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer): | |||||||
|  |  | ||||||
|         queryset = queryset.prefetch_related('sub_part') |         queryset = queryset.prefetch_related('sub_part') | ||||||
|         queryset = queryset.prefetch_related('sub_part__category') |         queryset = queryset.prefetch_related('sub_part__category') | ||||||
|  |         queryset = queryset.prefetch_related('sub_part__pricing_data') | ||||||
|  |  | ||||||
|         queryset = queryset.prefetch_related( |         queryset = queryset.prefetch_related( | ||||||
|             'sub_part__stock_items', |             'sub_part__stock_items', | ||||||
| @@ -1531,6 +1555,18 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer): | |||||||
|         available_stock = total_stock - build_order_allocations - sales_order_allocations |         available_stock = total_stock - build_order_allocations - sales_order_allocations | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|  |         # Annotate with the 'total pricing' information based on unit pricing and quantity | ||||||
|  |         queryset = queryset.annotate( | ||||||
|  |             pricing_min_total=ExpressionWrapper( | ||||||
|  |                 F('quantity') * F('sub_part__pricing_data__overall_min'), | ||||||
|  |                 output_field=models.DecimalField(), | ||||||
|  |             ), | ||||||
|  |             pricing_max_total=ExpressionWrapper( | ||||||
|  |                 F('quantity') * F('sub_part__pricing_data__overall_max'), | ||||||
|  |                 output_field=models.DecimalField(), | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         ref = 'sub_part__' |         ref = 'sub_part__' | ||||||
|  |  | ||||||
|         # Annotate with the total "on order" amount for the sub-part |         # Annotate with the total "on order" amount for the sub-part | ||||||
|   | |||||||
| @@ -49,6 +49,7 @@ | |||||||
|         "react-router-dom": "^6.22.1", |         "react-router-dom": "^6.22.1", | ||||||
|         "react-select": "^5.8.0", |         "react-select": "^5.8.0", | ||||||
|         "react-simplemde-editor": "^5.2.0", |         "react-simplemde-editor": "^5.2.0", | ||||||
|  |         "recharts": "^2.12.4", | ||||||
|         "styled-components": "^5.3.6", |         "styled-components": "^5.3.6", | ||||||
|         "zustand": "^4.5.1" |         "zustand": "^4.5.1" | ||||||
|     }, |     }, | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								src/frontend/src/components/charts/colors.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/frontend/src/components/charts/colors.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | export const CHART_COLORS: string[] = [ | ||||||
|  |   '#ffa8a8', | ||||||
|  |   '#8ce99a', | ||||||
|  |   '#74c0fc', | ||||||
|  |   '#ffe066', | ||||||
|  |   '#63e6be', | ||||||
|  |   '#ffc078', | ||||||
|  |   '#d8f5a2', | ||||||
|  |   '#66d9e8', | ||||||
|  |   '#e599f7', | ||||||
|  |   '#dee2e6' | ||||||
|  | ]; | ||||||
| @@ -230,17 +230,10 @@ export function ApiFormField({ | |||||||
|             id={fieldId} |             id={fieldId} | ||||||
|             value={numericalValue} |             value={numericalValue} | ||||||
|             error={error?.message} |             error={error?.message} | ||||||
|             formatter={(value) => { |  | ||||||
|               let v: any = parseFloat(value); |  | ||||||
|  |  | ||||||
|               if (Number.isNaN(v) || !Number.isFinite(v)) { |  | ||||||
|                 return value; |  | ||||||
|               } |  | ||||||
|  |  | ||||||
|               return `${1 * v.toFixed()}`; |  | ||||||
|             }} |  | ||||||
|             precision={definition.field_type == 'integer' ? 0 : 10} |             precision={definition.field_type == 'integer' ? 0 : 10} | ||||||
|             onChange={(value: number) => onChange(value)} |             onChange={(value: number) => onChange(value)} | ||||||
|  |             removeTrailingZeros | ||||||
|  |             step={1} | ||||||
|           /> |           /> | ||||||
|         ); |         ); | ||||||
|       case 'choice': |       case 'choice': | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ export function Thumbnail({ | |||||||
|   src, |   src, | ||||||
|   alt = t`Thumbnail`, |   alt = t`Thumbnail`, | ||||||
|   size = 20, |   size = 20, | ||||||
|  |   link, | ||||||
|   text, |   text, | ||||||
|   align |   align | ||||||
| }: { | }: { | ||||||
| @@ -21,9 +22,22 @@ export function Thumbnail({ | |||||||
|   size?: number; |   size?: number; | ||||||
|   text?: ReactNode; |   text?: ReactNode; | ||||||
|   align?: string; |   align?: string; | ||||||
|  |   link?: string; | ||||||
| }) { | }) { | ||||||
|   const backup_image = '/static/img/blank_image.png'; |   const backup_image = '/static/img/blank_image.png'; | ||||||
|  |  | ||||||
|  |   const inner = useMemo(() => { | ||||||
|  |     if (link) { | ||||||
|  |       return ( | ||||||
|  |         <Anchor href={link} target="_blank"> | ||||||
|  |           {text} | ||||||
|  |         </Anchor> | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       return text; | ||||||
|  |     } | ||||||
|  |   }, [link, text]); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Group align={align ?? 'left'} spacing="xs" noWrap={true}> |     <Group align={align ?? 'left'} spacing="xs" noWrap={true}> | ||||||
|       <ApiImage |       <ApiImage | ||||||
| @@ -39,7 +53,7 @@ export function Thumbnail({ | |||||||
|           } |           } | ||||||
|         }} |         }} | ||||||
|       /> |       /> | ||||||
|       {text} |       {inner} | ||||||
|     </Group> |     </Group> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,11 +5,33 @@ import { | |||||||
|   useUserSettingsState |   useUserSettingsState | ||||||
| } from '../states/SettingsState'; | } from '../states/SettingsState'; | ||||||
|  |  | ||||||
|  | interface formatDecmimalOptionsType { | ||||||
|  |   digits?: number; | ||||||
|  |   minDigits?: number; | ||||||
|  |   locale?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
| interface formatCurrencyOptionsType { | interface formatCurrencyOptionsType { | ||||||
|   digits?: number; |   digits?: number; | ||||||
|   minDigits?: number; |   minDigits?: number; | ||||||
|   currency?: string; |   currency?: string; | ||||||
|   locale?: string; |   locale?: string; | ||||||
|  |   multiplier?: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function formatDecimal( | ||||||
|  |   value: number | null | undefined, | ||||||
|  |   options: formatDecmimalOptionsType = {} | ||||||
|  | ) { | ||||||
|  |   let locale = options.locale || navigator.language || 'en-US'; | ||||||
|  |  | ||||||
|  |   if (value === null || value === undefined) { | ||||||
|  |     return value; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let formatter = new Intl.NumberFormat(locale); | ||||||
|  |  | ||||||
|  |   return formatter.format(value); | ||||||
| } | } | ||||||
|  |  | ||||||
| /* | /* | ||||||
| @@ -21,13 +43,21 @@ interface formatCurrencyOptionsType { | |||||||
|  * - digits: Maximum number of significant digits (default = 10) |  * - digits: Maximum number of significant digits (default = 10) | ||||||
|  */ |  */ | ||||||
| export function formatCurrency( | export function formatCurrency( | ||||||
|   value: number | null, |   value: number | string | null | undefined, | ||||||
|   options: formatCurrencyOptionsType = {} |   options: formatCurrencyOptionsType = {} | ||||||
| ) { | ) { | ||||||
|   if (value == null) { |   if (value == null || value == undefined) { | ||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   value = parseFloat(value.toString()); | ||||||
|  |  | ||||||
|  |   if (isNaN(value) || !isFinite(value)) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   value *= options.multiplier ?? 1; | ||||||
|  |  | ||||||
|   const global_settings = useGlobalSettingsState.getState().lookup; |   const global_settings = useGlobalSettingsState.getState().lookup; | ||||||
|  |  | ||||||
|   let maxDigits = options.digits || global_settings.PRICING_DECIMAL_PLACES || 6; |   let maxDigits = options.digits || global_settings.PRICING_DECIMAL_PLACES || 6; | ||||||
|   | |||||||
| @@ -61,6 +61,8 @@ export enum ApiEndpoints { | |||||||
|   part_parameter_template_list = 'part/parameter/template/', |   part_parameter_template_list = 'part/parameter/template/', | ||||||
|   part_thumbs_list = 'part/thumbs/', |   part_thumbs_list = 'part/thumbs/', | ||||||
|   part_pricing_get = 'part/:id/pricing/', |   part_pricing_get = 'part/:id/pricing/', | ||||||
|  |   part_pricing_internal = 'part/internal-price/', | ||||||
|  |   part_pricing_sale = 'part/sale-price/', | ||||||
|   part_stocktake_list = 'part/stocktake/', |   part_stocktake_list = 'part/stocktake/', | ||||||
|   category_list = 'part/category/', |   category_list = 'part/category/', | ||||||
|   category_tree = 'part/category/tree/', |   category_tree = 'part/category/tree/', | ||||||
| @@ -75,6 +77,7 @@ export enum ApiEndpoints { | |||||||
|   address_list = 'company/address/', |   address_list = 'company/address/', | ||||||
|   company_attachment_list = 'company/attachment/', |   company_attachment_list = 'company/attachment/', | ||||||
|   supplier_part_list = 'company/part/', |   supplier_part_list = 'company/part/', | ||||||
|  |   supplier_part_pricing_list = 'company/price-break/', | ||||||
|   manufacturer_part_list = 'company/part/manufacturer/', |   manufacturer_part_list = 'company/part/manufacturer/', | ||||||
|   manufacturer_part_attachment_list = 'company/part/manufacturer/attachment/', |   manufacturer_part_attachment_list = 'company/part/manufacturer/attachment/', | ||||||
|   manufacturer_part_parameter_list = 'company/part/manufacturer/parameter/', |   manufacturer_part_parameter_list = 'company/part/manufacturer/parameter/', | ||||||
| @@ -101,9 +104,12 @@ export enum ApiEndpoints { | |||||||
|   purchase_order_line_list = 'order/po-line/', |   purchase_order_line_list = 'order/po-line/', | ||||||
|   purchase_order_attachment_list = 'order/po/attachment/', |   purchase_order_attachment_list = 'order/po/attachment/', | ||||||
|   purchase_order_receive = 'order/po/:id/receive/', |   purchase_order_receive = 'order/po/:id/receive/', | ||||||
|  |  | ||||||
|   sales_order_list = 'order/so/', |   sales_order_list = 'order/so/', | ||||||
|  |   sales_order_line_list = 'order/so-line/', | ||||||
|   sales_order_attachment_list = 'order/so/attachment/', |   sales_order_attachment_list = 'order/so/attachment/', | ||||||
|   sales_order_shipment_list = 'order/so/shipment/', |   sales_order_shipment_list = 'order/so/shipment/', | ||||||
|  |  | ||||||
|   return_order_list = 'order/ro/', |   return_order_list = 'order/ro/', | ||||||
|   return_order_attachment_list = 'order/ro/attachment/', |   return_order_attachment_list = 'order/ro/attachment/', | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,15 @@ | |||||||
| import { ModelInformationDict } from '../components/render/ModelType'; | import { ModelInformationDict } from '../components/render/ModelType'; | ||||||
| import { ModelType } from '../enums/ModelType'; | import { ModelType } from '../enums/ModelType'; | ||||||
|  | import { base_url } from '../main'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Returns the detail view URL for a given model type |  * Returns the detail view URL for a given model type | ||||||
|  */ |  */ | ||||||
| export function getDetailUrl(model: ModelType, pk: number | string): string { | export function getDetailUrl( | ||||||
|  |   model: ModelType, | ||||||
|  |   pk: number | string, | ||||||
|  |   absolute?: boolean | ||||||
|  | ): string { | ||||||
|   const modelInfo = ModelInformationDict[model]; |   const modelInfo = ModelInformationDict[model]; | ||||||
|  |  | ||||||
|   if (pk === undefined || pk === null) { |   if (pk === undefined || pk === null) { | ||||||
| @@ -12,7 +17,14 @@ export function getDetailUrl(model: ModelType, pk: number | string): string { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (!!pk && modelInfo && modelInfo.url_detail) { |   if (!!pk && modelInfo && modelInfo.url_detail) { | ||||||
|     return modelInfo.url_detail.replace(':pk', pk.toString()); |     let url = modelInfo.url_detail.replace(':pk', pk.toString()); | ||||||
|  |     let base = base_url; | ||||||
|  |  | ||||||
|  |     if (absolute && base) { | ||||||
|  |       return `/${base}${url}`; | ||||||
|  |     } else { | ||||||
|  |       return url; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   console.error(`No detail URL found for model ${model} <${pk}>`); |   console.error(`No detail URL found for model ${model} <${pk}>`); | ||||||
|   | |||||||
| @@ -30,6 +30,8 @@ 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 { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable'; | import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable'; | ||||||
|  | import SupplierPriceBreakTable from '../../tables/purchasing/SupplierPriceBreakTable'; | ||||||
|  | import { StockItemTable } from '../../tables/stock/StockItemTable'; | ||||||
|  |  | ||||||
| export default function SupplierPartDetail() { | export default function SupplierPartDetail() { | ||||||
|   const { id } = useParams(); |   const { id } = useParams(); | ||||||
| @@ -201,7 +203,16 @@ export default function SupplierPartDetail() { | |||||||
|       { |       { | ||||||
|         name: 'stock', |         name: 'stock', | ||||||
|         label: t`Received Stock`, |         label: t`Received Stock`, | ||||||
|         icon: <IconPackages /> |         icon: <IconPackages />, | ||||||
|  |         content: supplierPart?.pk ? ( | ||||||
|  |           <StockItemTable | ||||||
|  |             tableName="supplier-stock" | ||||||
|  |             allowAdd={false} | ||||||
|  |             params={{ supplier_part: supplierPart.pk }} | ||||||
|  |           /> | ||||||
|  |         ) : ( | ||||||
|  |           <Skeleton /> | ||||||
|  |         ) | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         name: 'purchaseorders', |         name: 'purchaseorders', | ||||||
| @@ -215,8 +226,13 @@ export default function SupplierPartDetail() { | |||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         name: 'pricing', |         name: 'pricing', | ||||||
|         label: t`Pricing`, |         label: t`Supplier Pricing`, | ||||||
|         icon: <IconCurrencyDollar /> |         icon: <IconCurrencyDollar />, | ||||||
|  |         content: supplierPart?.pk ? ( | ||||||
|  |           <SupplierPriceBreakTable supplierPartId={supplierPart.pk} /> | ||||||
|  |         ) : ( | ||||||
|  |           <Skeleton /> | ||||||
|  |         ) | ||||||
|       } |       } | ||||||
|     ]; |     ]; | ||||||
|   }, [supplierPart]); |   }, [supplierPart]); | ||||||
|   | |||||||
| @@ -84,6 +84,7 @@ import { ManufacturerPartTable } from '../../tables/purchasing/ManufacturerPartT | |||||||
| import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable'; | import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable'; | ||||||
| import { SalesOrderTable } from '../../tables/sales/SalesOrderTable'; | import { SalesOrderTable } from '../../tables/sales/SalesOrderTable'; | ||||||
| import { StockItemTable } from '../../tables/stock/StockItemTable'; | import { StockItemTable } from '../../tables/stock/StockItemTable'; | ||||||
|  | import PartPricingPanel from './PartPricingPanel'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Detail view for a single Part instance |  * Detail view for a single Part instance | ||||||
| @@ -530,8 +531,9 @@ export default function PartDetail() { | |||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         name: 'pricing', |         name: 'pricing', | ||||||
|         label: t`Pricing`, |         label: t`Part Pricing`, | ||||||
|         icon: <IconCurrencyDollar /> |         icon: <IconCurrencyDollar />, | ||||||
|  |         content: part ? <PartPricingPanel part={part} /> : <Skeleton /> | ||||||
|       }, |       }, | ||||||
|       { |       { | ||||||
|         name: 'manufacturers', |         name: 'manufacturers', | ||||||
|   | |||||||
							
								
								
									
										115
									
								
								src/frontend/src/pages/part/PartPricingPanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/frontend/src/pages/part/PartPricingPanel.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | |||||||
|  | import { t } from '@lingui/macro'; | ||||||
|  | import { Accordion, Alert, LoadingOverlay, Stack, Text } from '@mantine/core'; | ||||||
|  | import { ReactNode, useMemo } from 'react'; | ||||||
|  |  | ||||||
|  | import { ApiEndpoints } from '../../enums/ApiEndpoints'; | ||||||
|  | import { UserRoles } from '../../enums/Roles'; | ||||||
|  | import { useInstance } from '../../hooks/UseInstance'; | ||||||
|  | import { useUserState } from '../../states/UserState'; | ||||||
|  | import BomPricingPanel from './pricing/BomPricingPanel'; | ||||||
|  | import PriceBreakPanel from './pricing/PriceBreakPanel'; | ||||||
|  | import PricingOverviewPanel from './pricing/PricingOverviewPanel'; | ||||||
|  | import PricingPanel from './pricing/PricingPanel'; | ||||||
|  | import PurchaseHistoryPanel from './pricing/PurchaseHistoryPanel'; | ||||||
|  | import SaleHistoryPanel from './pricing/SaleHistoryPanel'; | ||||||
|  | import SupplierPricingPanel from './pricing/SupplierPricingPanel'; | ||||||
|  | import VariantPricingPanel from './pricing/VariantPricingPanel'; | ||||||
|  |  | ||||||
|  | export default function PartPricingPanel({ part }: { part: any }) { | ||||||
|  |   const user = useUserState(); | ||||||
|  |  | ||||||
|  |   const { | ||||||
|  |     instance: pricing, | ||||||
|  |     refreshInstance, | ||||||
|  |     instanceQuery | ||||||
|  |   } = useInstance({ | ||||||
|  |     pk: part?.pk, | ||||||
|  |     hasPrimaryKey: true, | ||||||
|  |     endpoint: ApiEndpoints.part_pricing_get, | ||||||
|  |     defaultValue: {} | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // TODO: Do we display internal price? This is a global setting | ||||||
|  |   const internalPricing = true; | ||||||
|  |  | ||||||
|  |   const purchaseOrderPricing = useMemo(() => { | ||||||
|  |     return user.hasViewRole(UserRoles.purchase_order) && part?.purchaseable; | ||||||
|  |   }, [user, part]); | ||||||
|  |  | ||||||
|  |   const salesOrderPricing = useMemo(() => { | ||||||
|  |     return user.hasViewRole(UserRoles.sales_order) && part?.salable; | ||||||
|  |   }, [user, part]); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Stack spacing="xs"> | ||||||
|  |       <LoadingOverlay visible={instanceQuery.isLoading} /> | ||||||
|  |       {!pricing && !instanceQuery.isLoading && ( | ||||||
|  |         <Alert color="ref" title={t`Error`}> | ||||||
|  |           <Text>{t`No pricing data found for this part.`}</Text> | ||||||
|  |         </Alert> | ||||||
|  |       )} | ||||||
|  |       {pricing && ( | ||||||
|  |         <Accordion multiple defaultValue={['overview']}> | ||||||
|  |           <PricingPanel | ||||||
|  |             content={<PricingOverviewPanel part={part} pricing={pricing} />} | ||||||
|  |             label="overview" | ||||||
|  |             title={t`Pricing Overview`} | ||||||
|  |             visible={true} | ||||||
|  |           /> | ||||||
|  |           <PricingPanel | ||||||
|  |             content={<PurchaseHistoryPanel part={part} />} | ||||||
|  |             label="purchase" | ||||||
|  |             title={t`Purchase History`} | ||||||
|  |             visible={purchaseOrderPricing} | ||||||
|  |           /> | ||||||
|  |           <PricingPanel | ||||||
|  |             content={ | ||||||
|  |               <PriceBreakPanel | ||||||
|  |                 part={part} | ||||||
|  |                 endpoint={ApiEndpoints.part_pricing_internal} | ||||||
|  |               /> | ||||||
|  |             } | ||||||
|  |             label="internal" | ||||||
|  |             title={t`Internal Pricing`} | ||||||
|  |             visible={internalPricing} | ||||||
|  |           /> | ||||||
|  |           <PricingPanel | ||||||
|  |             content={<SupplierPricingPanel part={part} />} | ||||||
|  |             label="supplier" | ||||||
|  |             title={t`Supplier Pricing`} | ||||||
|  |             visible={purchaseOrderPricing} | ||||||
|  |           /> | ||||||
|  |           <PricingPanel | ||||||
|  |             content={<BomPricingPanel part={part} pricing={pricing} />} | ||||||
|  |             label="bom" | ||||||
|  |             title={t`BOM Pricing`} | ||||||
|  |             visible={part?.assembly} | ||||||
|  |           /> | ||||||
|  |           <PricingPanel | ||||||
|  |             content={<VariantPricingPanel part={part} pricing={pricing} />} | ||||||
|  |             label="variant" | ||||||
|  |             title={t`Variant Pricing`} | ||||||
|  |             visible={part?.is_template} | ||||||
|  |           /> | ||||||
|  |           <PricingPanel | ||||||
|  |             content={ | ||||||
|  |               <PriceBreakPanel | ||||||
|  |                 part={part} | ||||||
|  |                 endpoint={ApiEndpoints.part_pricing_sale} | ||||||
|  |               /> | ||||||
|  |             } | ||||||
|  |             label="sale-pricing" | ||||||
|  |             title={t`Sale Pricing`} | ||||||
|  |             visible={salesOrderPricing} | ||||||
|  |           /> | ||||||
|  |           <PricingPanel | ||||||
|  |             content={<SaleHistoryPanel part={part} />} | ||||||
|  |             label="sale-history" | ||||||
|  |             title={t`Sale History`} | ||||||
|  |             visible={salesOrderPricing} | ||||||
|  |           /> | ||||||
|  |         </Accordion> | ||||||
|  |       )} | ||||||
|  |     </Stack> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										203
									
								
								src/frontend/src/pages/part/pricing/BomPricingPanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								src/frontend/src/pages/part/pricing/BomPricingPanel.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,203 @@ | |||||||
|  | import { t } from '@lingui/macro'; | ||||||
|  | import { SegmentedControl, SimpleGrid, Stack } from '@mantine/core'; | ||||||
|  | import { ReactNode, useMemo, useState } from 'react'; | ||||||
|  | import { | ||||||
|  |   Bar, | ||||||
|  |   BarChart, | ||||||
|  |   Cell, | ||||||
|  |   Legend, | ||||||
|  |   Pie, | ||||||
|  |   PieChart, | ||||||
|  |   ResponsiveContainer, | ||||||
|  |   Tooltip, | ||||||
|  |   XAxis, | ||||||
|  |   YAxis | ||||||
|  | } from 'recharts'; | ||||||
|  |  | ||||||
|  | import { CHART_COLORS } from '../../../components/charts/colors'; | ||||||
|  | import { formatDecimal, formatPriceRange } from '../../../defaults/formatters'; | ||||||
|  | import { ApiEndpoints } from '../../../enums/ApiEndpoints'; | ||||||
|  | import { useTable } from '../../../hooks/UseTable'; | ||||||
|  | import { apiUrl } from '../../../states/ApiState'; | ||||||
|  | import { TableColumn } from '../../../tables/Column'; | ||||||
|  | import { DateColumn, PartColumn } from '../../../tables/ColumnRenderers'; | ||||||
|  | import { InvenTreeTable } from '../../../tables/InvenTreeTable'; | ||||||
|  | import { NoPricingData } from './PricingPanel'; | ||||||
|  |  | ||||||
|  | // Display BOM data as a pie chart | ||||||
|  | function BomPieChart({ data }: { data: any[] }) { | ||||||
|  |   return ( | ||||||
|  |     <ResponsiveContainer width="100%" height={500}> | ||||||
|  |       <PieChart> | ||||||
|  |         <Pie | ||||||
|  |           data={data} | ||||||
|  |           dataKey="total_price_min" | ||||||
|  |           nameKey="name" | ||||||
|  |           innerRadius={20} | ||||||
|  |           outerRadius={100} | ||||||
|  |         > | ||||||
|  |           {data.map((_entry, index) => ( | ||||||
|  |             <Cell | ||||||
|  |               key={`cell-${index}`} | ||||||
|  |               fill={CHART_COLORS[index % CHART_COLORS.length]} | ||||||
|  |             /> | ||||||
|  |           ))} | ||||||
|  |         </Pie> | ||||||
|  |         <Pie | ||||||
|  |           data={data} | ||||||
|  |           dataKey="total_price_max" | ||||||
|  |           nameKey="name" | ||||||
|  |           innerRadius={120} | ||||||
|  |           outerRadius={240} | ||||||
|  |         > | ||||||
|  |           {data.map((_entry, index) => ( | ||||||
|  |             <Cell | ||||||
|  |               key={`cell-${index}`} | ||||||
|  |               fill={CHART_COLORS[index % CHART_COLORS.length]} | ||||||
|  |             /> | ||||||
|  |           ))} | ||||||
|  |         </Pie> | ||||||
|  |         <Tooltip /> | ||||||
|  |       </PieChart> | ||||||
|  |     </ResponsiveContainer> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Display BOM data as a bar chart | ||||||
|  | function BomBarChart({ data }: { data: any[] }) { | ||||||
|  |   return ( | ||||||
|  |     <ResponsiveContainer width="100%" height={500}> | ||||||
|  |       <BarChart data={data}> | ||||||
|  |         <XAxis dataKey="name" /> | ||||||
|  |         <YAxis /> | ||||||
|  |         <Tooltip /> | ||||||
|  |         <Legend /> | ||||||
|  |         <Bar | ||||||
|  |           dataKey="total_price_min" | ||||||
|  |           fill={CHART_COLORS[0]} | ||||||
|  |           label={t`Minimum Total Price`} | ||||||
|  |         /> | ||||||
|  |         <Bar | ||||||
|  |           dataKey="total_price_max" | ||||||
|  |           fill={CHART_COLORS[1]} | ||||||
|  |           label={t`Maximum Total Price`} | ||||||
|  |         /> | ||||||
|  |       </BarChart> | ||||||
|  |     </ResponsiveContainer> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default function BomPricingPanel({ | ||||||
|  |   part, | ||||||
|  |   pricing | ||||||
|  | }: { | ||||||
|  |   part: any; | ||||||
|  |   pricing: any; | ||||||
|  | }): ReactNode { | ||||||
|  |   const table = useTable('pricing-bom'); | ||||||
|  |  | ||||||
|  |   const columns: TableColumn[] = useMemo(() => { | ||||||
|  |     return [ | ||||||
|  |       { | ||||||
|  |         accessor: 'name', | ||||||
|  |         title: t`Component`, | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: false, | ||||||
|  |         render: (record: any) => PartColumn(record.sub_part_detail) | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         accessor: 'quantity', | ||||||
|  |         title: t`Quantity`, | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: false, | ||||||
|  |         render: (record: any) => formatDecimal(record.quantity) | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         accessor: 'unit_price', | ||||||
|  |         ordering: 'pricing_max', | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: true, | ||||||
|  |         title: t`Unit Price`, | ||||||
|  |         render: (record: any) => { | ||||||
|  |           return formatPriceRange(record.pricing_min, record.pricing_max, { | ||||||
|  |             currency: pricing?.currency | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         accessor: 'total_price', | ||||||
|  |         title: t`Total Price`, | ||||||
|  |         ordering: 'pricing_max_total', | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: false, | ||||||
|  |         render: (record: any) => { | ||||||
|  |           return formatPriceRange( | ||||||
|  |             record.pricing_min_total, | ||||||
|  |             record.pricing_max_total, | ||||||
|  |             { | ||||||
|  |               currency: pricing?.currency | ||||||
|  |             } | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       DateColumn({ | ||||||
|  |         accessor: 'pricing_updated', | ||||||
|  |         title: t`Updated`, | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: true | ||||||
|  |       }) | ||||||
|  |     ]; | ||||||
|  |   }, [part, pricing]); | ||||||
|  |  | ||||||
|  |   const bomPricingData: any[] = useMemo(() => { | ||||||
|  |     const pricing = table.records.map((entry: any) => { | ||||||
|  |       return { | ||||||
|  |         name: entry.sub_part_detail?.name, | ||||||
|  |         unit_price_min: parseFloat(entry.pricing_min ?? 0), | ||||||
|  |         unit_price_max: parseFloat(entry.pricing_max ?? 0), | ||||||
|  |         total_price_min: parseFloat(entry.pricing_min_total ?? 0), | ||||||
|  |         total_price_max: parseFloat(entry.pricing_max_total ?? 0) | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return pricing; | ||||||
|  |   }, [table.records]); | ||||||
|  |  | ||||||
|  |   const [chartType, setChartType] = useState<string>('pie'); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Stack spacing="xs"> | ||||||
|  |       <SimpleGrid cols={2}> | ||||||
|  |         <InvenTreeTable | ||||||
|  |           tableState={table} | ||||||
|  |           url={apiUrl(ApiEndpoints.bom_list)} | ||||||
|  |           columns={columns} | ||||||
|  |           props={{ | ||||||
|  |             params: { | ||||||
|  |               part: part?.pk, | ||||||
|  |               sub_part_detail: true, | ||||||
|  |               has_pricing: true | ||||||
|  |             }, | ||||||
|  |             enableSelection: false | ||||||
|  |           }} | ||||||
|  |         /> | ||||||
|  |         {bomPricingData.length > 0 ? ( | ||||||
|  |           <Stack spacing="xs"> | ||||||
|  |             {chartType == 'bar' && <BomBarChart data={bomPricingData} />} | ||||||
|  |             {chartType == 'pie' && <BomPieChart data={bomPricingData} />} | ||||||
|  |             <SegmentedControl | ||||||
|  |               value={chartType} | ||||||
|  |               onChange={setChartType} | ||||||
|  |               data={[ | ||||||
|  |                 { value: 'pie', label: t`Pie Chart` }, | ||||||
|  |                 { value: 'bar', label: t`Bar Chart` } | ||||||
|  |               ]} | ||||||
|  |             /> | ||||||
|  |           </Stack> | ||||||
|  |         ) : ( | ||||||
|  |           <NoPricingData /> | ||||||
|  |         )} | ||||||
|  |       </SimpleGrid> | ||||||
|  |     </Stack> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										185
									
								
								src/frontend/src/pages/part/pricing/PriceBreakPanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								src/frontend/src/pages/part/pricing/PriceBreakPanel.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,185 @@ | |||||||
|  | import { t } from '@lingui/macro'; | ||||||
|  | import { Alert, SimpleGrid } from '@mantine/core'; | ||||||
|  | import { useCallback, useMemo, useState } from 'react'; | ||||||
|  | import { | ||||||
|  |   Bar, | ||||||
|  |   BarChart, | ||||||
|  |   Legend, | ||||||
|  |   ResponsiveContainer, | ||||||
|  |   Tooltip, | ||||||
|  |   XAxis, | ||||||
|  |   YAxis | ||||||
|  | } from 'recharts'; | ||||||
|  |  | ||||||
|  | import { AddItemButton } from '../../../components/buttons/AddItemButton'; | ||||||
|  | import { CHART_COLORS } from '../../../components/charts/colors'; | ||||||
|  | import { ApiFormFieldSet } from '../../../components/forms/fields/ApiFormField'; | ||||||
|  | import { formatCurrency } from '../../../defaults/formatters'; | ||||||
|  | import { ApiEndpoints } from '../../../enums/ApiEndpoints'; | ||||||
|  | import { UserRoles } from '../../../enums/Roles'; | ||||||
|  | import { | ||||||
|  |   useCreateApiFormModal, | ||||||
|  |   useDeleteApiFormModal, | ||||||
|  |   useEditApiFormModal | ||||||
|  | } from '../../../hooks/UseForm'; | ||||||
|  | import { useTable } from '../../../hooks/UseTable'; | ||||||
|  | import { apiUrl } from '../../../states/ApiState'; | ||||||
|  | import { useUserState } from '../../../states/UserState'; | ||||||
|  | import { TableColumn } from '../../../tables/Column'; | ||||||
|  | import { InvenTreeTable } from '../../../tables/InvenTreeTable'; | ||||||
|  | import { RowDeleteAction, RowEditAction } from '../../../tables/RowActions'; | ||||||
|  | import { NoPricingData } from './PricingPanel'; | ||||||
|  |  | ||||||
|  | export default function PriceBreakPanel({ | ||||||
|  |   part, | ||||||
|  |   endpoint | ||||||
|  | }: { | ||||||
|  |   part: any; | ||||||
|  |   endpoint: ApiEndpoints; | ||||||
|  | }) { | ||||||
|  |   const user = useUserState(); | ||||||
|  |   const table = useTable('pricing-internal'); | ||||||
|  |  | ||||||
|  |   const priceBreakFields: ApiFormFieldSet = useMemo(() => { | ||||||
|  |     return { | ||||||
|  |       part: { | ||||||
|  |         disabled: true | ||||||
|  |       }, | ||||||
|  |       quantity: {}, | ||||||
|  |       price: {}, | ||||||
|  |       price_currency: {} | ||||||
|  |     }; | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   const tableUrl = useMemo(() => { | ||||||
|  |     return apiUrl(endpoint); | ||||||
|  |   }, [endpoint]); | ||||||
|  |  | ||||||
|  |   const [selectedPriceBreak, setSelectedPriceBreak] = useState<number>(0); | ||||||
|  |  | ||||||
|  |   const newPriceBreak = useCreateApiFormModal({ | ||||||
|  |     url: tableUrl, | ||||||
|  |     title: t`Add Price Break`, | ||||||
|  |     fields: priceBreakFields, | ||||||
|  |     initialData: { | ||||||
|  |       part: part.pk | ||||||
|  |     }, | ||||||
|  |     onFormSuccess: (data: any) => { | ||||||
|  |       table.updateRecord(data); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const editPriceBreak = useEditApiFormModal({ | ||||||
|  |     url: tableUrl, | ||||||
|  |     pk: selectedPriceBreak, | ||||||
|  |     title: t`Edit Price Break`, | ||||||
|  |     fields: priceBreakFields, | ||||||
|  |     onFormSuccess: (data: any) => { | ||||||
|  |       table.updateRecord(data); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const deletePriceBreak = useDeleteApiFormModal({ | ||||||
|  |     url: tableUrl, | ||||||
|  |     pk: selectedPriceBreak, | ||||||
|  |     title: t`Delete Price Break`, | ||||||
|  |     onFormSuccess: () => { | ||||||
|  |       table.refreshTable(); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const columns: TableColumn[] = useMemo(() => { | ||||||
|  |     return [ | ||||||
|  |       { | ||||||
|  |         accessor: 'quantity', | ||||||
|  |         title: t`Quantity`, | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: false | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         accessor: 'price', | ||||||
|  |         title: t`Price Break`, | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: false, | ||||||
|  |         render: (record: any) => { | ||||||
|  |           return formatCurrency(record.price, { | ||||||
|  |             currency: record.price_currency | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ]; | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   const tableActions = useMemo(() => { | ||||||
|  |     return [ | ||||||
|  |       <AddItemButton | ||||||
|  |         tooltip={t`Add Price Break`} | ||||||
|  |         onClick={() => { | ||||||
|  |           newPriceBreak.open(); | ||||||
|  |         }} | ||||||
|  |         hidden={!user.hasAddRole(UserRoles.part)} | ||||||
|  |       /> | ||||||
|  |     ]; | ||||||
|  |   }, [user]); | ||||||
|  |  | ||||||
|  |   const rowActions = useCallback( | ||||||
|  |     (record: any) => { | ||||||
|  |       return [ | ||||||
|  |         RowEditAction({ | ||||||
|  |           hidden: !user.hasChangeRole(UserRoles.part), | ||||||
|  |           onClick: () => { | ||||||
|  |             setSelectedPriceBreak(record.pk); | ||||||
|  |             editPriceBreak.open(); | ||||||
|  |           } | ||||||
|  |         }), | ||||||
|  |         RowDeleteAction({ | ||||||
|  |           hidden: !user.hasDeleteRole(UserRoles.part), | ||||||
|  |           onClick: () => { | ||||||
|  |             setSelectedPriceBreak(record.pk); | ||||||
|  |             deletePriceBreak.open(); | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |       ]; | ||||||
|  |     }, | ||||||
|  |     [user] | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       {newPriceBreak.modal} | ||||||
|  |       {editPriceBreak.modal} | ||||||
|  |       {deletePriceBreak.modal} | ||||||
|  |       <SimpleGrid cols={2}> | ||||||
|  |         <InvenTreeTable | ||||||
|  |           tableState={table} | ||||||
|  |           url={tableUrl} | ||||||
|  |           columns={columns} | ||||||
|  |           props={{ | ||||||
|  |             params: { | ||||||
|  |               part: part.pk | ||||||
|  |             }, | ||||||
|  |             tableActions: tableActions, | ||||||
|  |             rowActions: rowActions | ||||||
|  |           }} | ||||||
|  |         /> | ||||||
|  |         {table.records.length > 0 ? ( | ||||||
|  |           <ResponsiveContainer width="100%" height={500}> | ||||||
|  |             <BarChart data={table.records}> | ||||||
|  |               <XAxis dataKey="quantity" /> | ||||||
|  |               <YAxis /> | ||||||
|  |               <Tooltip /> | ||||||
|  |               <Legend /> | ||||||
|  |               <Bar | ||||||
|  |                 dataKey="price" | ||||||
|  |                 fill={CHART_COLORS[0]} | ||||||
|  |                 label={t`Price Break`} | ||||||
|  |               /> | ||||||
|  |             </BarChart> | ||||||
|  |           </ResponsiveContainer> | ||||||
|  |         ) : ( | ||||||
|  |           <NoPricingData /> | ||||||
|  |         )} | ||||||
|  |       </SimpleGrid> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										179
									
								
								src/frontend/src/pages/part/pricing/PricingOverviewPanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								src/frontend/src/pages/part/pricing/PricingOverviewPanel.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,179 @@ | |||||||
|  | import { t } from '@lingui/macro'; | ||||||
|  | import { Alert, Group, Paper, SimpleGrid, Stack, Text } from '@mantine/core'; | ||||||
|  | import { | ||||||
|  |   IconBuildingWarehouse, | ||||||
|  |   IconChartDonut, | ||||||
|  |   IconExclamationCircle, | ||||||
|  |   IconList, | ||||||
|  |   IconReportAnalytics, | ||||||
|  |   IconShoppingCart, | ||||||
|  |   IconTriangleSquareCircle | ||||||
|  | } from '@tabler/icons-react'; | ||||||
|  | import { DataTable, DataTableColumn } from 'mantine-datatable'; | ||||||
|  | import { ReactNode, useMemo } from 'react'; | ||||||
|  | import { | ||||||
|  |   Bar, | ||||||
|  |   BarChart, | ||||||
|  |   Legend, | ||||||
|  |   ResponsiveContainer, | ||||||
|  |   Tooltip, | ||||||
|  |   XAxis, | ||||||
|  |   YAxis | ||||||
|  | } from 'recharts'; | ||||||
|  |  | ||||||
|  | import { CHART_COLORS } from '../../../components/charts/colors'; | ||||||
|  | import { formatCurrency, renderDate } from '../../../defaults/formatters'; | ||||||
|  |  | ||||||
|  | interface PricingOverviewEntry { | ||||||
|  |   icon: ReactNode; | ||||||
|  |   name: string; | ||||||
|  |   title: string; | ||||||
|  |   min_value: number | null | undefined; | ||||||
|  |   max_value: number | null | undefined; | ||||||
|  |   visible?: boolean; | ||||||
|  |   currency?: string | null | undefined; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default function PricingOverviewPanel({ | ||||||
|  |   part, | ||||||
|  |   pricing | ||||||
|  | }: { | ||||||
|  |   part: any; | ||||||
|  |   pricing: any; | ||||||
|  | }): ReactNode { | ||||||
|  |   const columns: DataTableColumn<any>[] = useMemo(() => { | ||||||
|  |     return [ | ||||||
|  |       { | ||||||
|  |         accessor: 'title', | ||||||
|  |         title: t`Pricing Category`, | ||||||
|  |         render: (record: PricingOverviewEntry) => { | ||||||
|  |           return ( | ||||||
|  |             <Group position="left" spacing="xs"> | ||||||
|  |               {record.icon} | ||||||
|  |               <Text weight={700}>{record.title}</Text> | ||||||
|  |             </Group> | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         accessor: 'min_value', | ||||||
|  |         title: t`Minimum`, | ||||||
|  |         render: (record: PricingOverviewEntry) => { | ||||||
|  |           if (record?.min_value === null || record?.min_value === undefined) { | ||||||
|  |             return '-'; | ||||||
|  |           } | ||||||
|  |           return formatCurrency(record?.min_value, { | ||||||
|  |             currency: record.currency ?? pricing?.currency | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         accessor: 'max_value', | ||||||
|  |         title: t`Maximum`, | ||||||
|  |         render: (record: PricingOverviewEntry) => { | ||||||
|  |           if (record?.max_value === null || record?.max_value === undefined) { | ||||||
|  |             return '-'; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           return formatCurrency(record?.max_value, { | ||||||
|  |             currency: record.currency ?? pricing?.currency | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ]; | ||||||
|  |   }, [part, pricing]); | ||||||
|  |  | ||||||
|  |   const overviewData: PricingOverviewEntry[] = useMemo(() => { | ||||||
|  |     return [ | ||||||
|  |       { | ||||||
|  |         name: 'internal', | ||||||
|  |         title: t`Internal Pricing`, | ||||||
|  |         icon: <IconList />, | ||||||
|  |         min_value: pricing?.internal_cost_min, | ||||||
|  |         max_value: pricing?.internal_cost_max | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'bom', | ||||||
|  |         title: t`BOM Pricing`, | ||||||
|  |         icon: <IconChartDonut />, | ||||||
|  |         min_value: pricing?.bom_cost_min, | ||||||
|  |         max_value: pricing?.bom_cost_max | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'purchase', | ||||||
|  |         title: t`Purchase Pricing`, | ||||||
|  |         icon: <IconShoppingCart />, | ||||||
|  |         min_value: pricing?.purchase_cost_min, | ||||||
|  |         max_value: pricing?.purchase_cost_max | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'supplier', | ||||||
|  |         title: t`Supplier Pricing`, | ||||||
|  |         icon: <IconBuildingWarehouse />, | ||||||
|  |         min_value: pricing?.supplier_price_min, | ||||||
|  |         max_value: pricing?.supplier_price_max | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'variants', | ||||||
|  |         title: t`Variant Pricing`, | ||||||
|  |         icon: <IconTriangleSquareCircle />, | ||||||
|  |         min_value: pricing?.variant_cost_min, | ||||||
|  |         max_value: pricing?.variant_cost_max | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'override', | ||||||
|  |         title: t`Override Pricing`, | ||||||
|  |         icon: <IconExclamationCircle />, | ||||||
|  |         min_value: pricing?.override_min, | ||||||
|  |         max_value: pricing?.override_max | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         name: 'overall', | ||||||
|  |         title: t`Overall Pricing`, | ||||||
|  |         icon: <IconReportAnalytics />, | ||||||
|  |         min_value: pricing?.overall_min, | ||||||
|  |         max_value: pricing?.overall_max | ||||||
|  |       } | ||||||
|  |     ].filter((entry) => { | ||||||
|  |       return entry.min_value !== null || entry.max_value !== null; | ||||||
|  |     }); | ||||||
|  |   }, [part, pricing]); | ||||||
|  |  | ||||||
|  |   // TODO: Add display of "last updated" | ||||||
|  |   // TODO: Add "update now" button | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Stack spacing="xs"> | ||||||
|  |       <SimpleGrid cols={2}> | ||||||
|  |         <Stack spacing="xs"> | ||||||
|  |           {pricing?.updated && ( | ||||||
|  |             <Paper p="xs"> | ||||||
|  |               <Alert color="blue" title={t`Last Updated`}> | ||||||
|  |                 <Text>{renderDate(pricing.updated)}</Text> | ||||||
|  |               </Alert> | ||||||
|  |             </Paper> | ||||||
|  |           )} | ||||||
|  |           <DataTable records={overviewData} columns={columns} /> | ||||||
|  |         </Stack> | ||||||
|  |         <ResponsiveContainer width="100%" height={500}> | ||||||
|  |           <BarChart data={overviewData}> | ||||||
|  |             <XAxis dataKey="title" /> | ||||||
|  |             <YAxis /> | ||||||
|  |             <Tooltip /> | ||||||
|  |             <Legend /> | ||||||
|  |             <Bar | ||||||
|  |               dataKey="min_value" | ||||||
|  |               fill={CHART_COLORS[0]} | ||||||
|  |               label={t`Minimum Price`} | ||||||
|  |             /> | ||||||
|  |             <Bar | ||||||
|  |               dataKey="max_value" | ||||||
|  |               fill={CHART_COLORS[1]} | ||||||
|  |               label={t`Maximum Price`} | ||||||
|  |             /> | ||||||
|  |           </BarChart> | ||||||
|  |         </ResponsiveContainer> | ||||||
|  |       </SimpleGrid> | ||||||
|  |     </Stack> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								src/frontend/src/pages/part/pricing/PricingPanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/frontend/src/pages/part/pricing/PricingPanel.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | import { t } from '@lingui/macro'; | ||||||
|  | import { Accordion, Alert, Space, Stack, Text } from '@mantine/core'; | ||||||
|  | import { IconExclamationCircle } from '@tabler/icons-react'; | ||||||
|  | import { ReactNode } from 'react'; | ||||||
|  |  | ||||||
|  | import { StylishText } from '../../../components/items/StylishText'; | ||||||
|  |  | ||||||
|  | export default function PricingPanel({ | ||||||
|  |   content, | ||||||
|  |   label, | ||||||
|  |   title, | ||||||
|  |   visible | ||||||
|  | }: { | ||||||
|  |   content: ReactNode; | ||||||
|  |   label: string; | ||||||
|  |   title: string; | ||||||
|  |   visible: boolean; | ||||||
|  | }): ReactNode { | ||||||
|  |   return ( | ||||||
|  |     visible && ( | ||||||
|  |       <Accordion.Item value={label}> | ||||||
|  |         <Accordion.Control> | ||||||
|  |           <StylishText size="lg">{title}</StylishText> | ||||||
|  |         </Accordion.Control> | ||||||
|  |         <Accordion.Panel>{content}</Accordion.Panel> | ||||||
|  |       </Accordion.Item> | ||||||
|  |     ) | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function NoPricingData() { | ||||||
|  |   return ( | ||||||
|  |     <Stack spacing="xs"> | ||||||
|  |       <Alert icon={<IconExclamationCircle />} color="blue" title={t`No Data`}> | ||||||
|  |         <Text>{t`No pricing data available`}</Text> | ||||||
|  |       </Alert> | ||||||
|  |       <Space /> | ||||||
|  |     </Stack> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										149
									
								
								src/frontend/src/pages/part/pricing/PurchaseHistoryPanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								src/frontend/src/pages/part/pricing/PurchaseHistoryPanel.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | |||||||
|  | import { t } from '@lingui/macro'; | ||||||
|  | import { Group, SimpleGrid, Text } from '@mantine/core'; | ||||||
|  | import { ReactNode, useCallback, useMemo } from 'react'; | ||||||
|  | import { | ||||||
|  |   Bar, | ||||||
|  |   BarChart, | ||||||
|  |   Legend, | ||||||
|  |   ResponsiveContainer, | ||||||
|  |   Tooltip, | ||||||
|  |   XAxis, | ||||||
|  |   YAxis | ||||||
|  | } from 'recharts'; | ||||||
|  |  | ||||||
|  | import { CHART_COLORS } from '../../../components/charts/colors'; | ||||||
|  | import { formatCurrency, renderDate } from '../../../defaults/formatters'; | ||||||
|  | import { ApiEndpoints } from '../../../enums/ApiEndpoints'; | ||||||
|  | import { useTable } from '../../../hooks/UseTable'; | ||||||
|  | import { apiUrl } from '../../../states/ApiState'; | ||||||
|  | import { TableColumn } from '../../../tables/Column'; | ||||||
|  | import { InvenTreeTable } from '../../../tables/InvenTreeTable'; | ||||||
|  | import { NoPricingData } from './PricingPanel'; | ||||||
|  |  | ||||||
|  | export default function PurchaseHistoryPanel({ | ||||||
|  |   part | ||||||
|  | }: { | ||||||
|  |   part: any; | ||||||
|  | }): ReactNode { | ||||||
|  |   const table = useTable('pricing-purchase-history'); | ||||||
|  |  | ||||||
|  |   const calculateUnitPrice = useCallback((record: any) => { | ||||||
|  |     let pack_quantity = record?.supplier_part_detail?.pack_quantity_native ?? 1; | ||||||
|  |     let unit_price = record.purchase_price / pack_quantity; | ||||||
|  |  | ||||||
|  |     return unit_price; | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   const columns: TableColumn[] = useMemo(() => { | ||||||
|  |     return [ | ||||||
|  |       { | ||||||
|  |         accessor: 'order', | ||||||
|  |         title: t`Purchase Order`, | ||||||
|  |         render: (record: any) => record?.order_detail?.reference, | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: false | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         accessor: 'order_detail.complete_date', | ||||||
|  |         ordering: 'complete_date', | ||||||
|  |         title: t`Date`, | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: true, | ||||||
|  |         render: (record: any) => renderDate(record.order_detail.complete_date) | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         accessor: 'purchase_price', | ||||||
|  |         title: t`Purchase Price`, | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: false, | ||||||
|  |         render: (record: any) => { | ||||||
|  |           let price = formatCurrency(record.purchase_price, { | ||||||
|  |             currency: record.purchase_price_currency | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           let units = record.supplier_part_detail?.pack_quantity; | ||||||
|  |  | ||||||
|  |           return ( | ||||||
|  |             <Group position="apart" spacing="xs"> | ||||||
|  |               <Text>{price}</Text> | ||||||
|  |               {units && <Text size="xs">[{units}]</Text>} | ||||||
|  |             </Group> | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         accessor: 'unit_price', | ||||||
|  |         title: t`Unit Price`, | ||||||
|  |         ordering: 'purchase_price', | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: false, | ||||||
|  |         render: (record: any) => { | ||||||
|  |           let price = formatCurrency(calculateUnitPrice(record), { | ||||||
|  |             currency: record.purchase_price_currency | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           let units = record.part_detail?.units; | ||||||
|  |  | ||||||
|  |           return ( | ||||||
|  |             <Group position="apart" spacing="xs"> | ||||||
|  |               <Text>{price}</Text> | ||||||
|  |               {units && <Text size="xs">[{units}]</Text>} | ||||||
|  |             </Group> | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ]; | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   const purchaseHistoryData = useMemo(() => { | ||||||
|  |     return table.records.map((record: any) => { | ||||||
|  |       return { | ||||||
|  |         quantity: record.quantity, | ||||||
|  |         purchase_price: record.purchase_price, | ||||||
|  |         unit_price: calculateUnitPrice(record), | ||||||
|  |         name: record.order_detail.reference | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |   }, [table.records]); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <SimpleGrid cols={2}> | ||||||
|  |       <InvenTreeTable | ||||||
|  |         tableState={table} | ||||||
|  |         url={apiUrl(ApiEndpoints.purchase_order_line_list)} | ||||||
|  |         columns={columns} | ||||||
|  |         props={{ | ||||||
|  |           params: { | ||||||
|  |             base_part: part.pk, | ||||||
|  |             part_detail: true, | ||||||
|  |             order_detail: true, | ||||||
|  |             has_pricing: true, | ||||||
|  |             order_complete: true | ||||||
|  |           } | ||||||
|  |         }} | ||||||
|  |       /> | ||||||
|  |       {purchaseHistoryData.length > 0 ? ( | ||||||
|  |         <ResponsiveContainer width="100%" height={500}> | ||||||
|  |           <BarChart data={purchaseHistoryData}> | ||||||
|  |             <XAxis dataKey="name" /> | ||||||
|  |             <YAxis /> | ||||||
|  |             <Tooltip /> | ||||||
|  |             <Legend /> | ||||||
|  |             <Bar | ||||||
|  |               dataKey="unit_price" | ||||||
|  |               fill={CHART_COLORS[0]} | ||||||
|  |               label={t`Unit Price`} | ||||||
|  |             /> | ||||||
|  |             <Bar | ||||||
|  |               dataKey="purchase_price" | ||||||
|  |               fill={CHART_COLORS[1]} | ||||||
|  |               label={t`Purchase Price`} | ||||||
|  |             /> | ||||||
|  |           </BarChart> | ||||||
|  |         </ResponsiveContainer> | ||||||
|  |       ) : ( | ||||||
|  |         <NoPricingData /> | ||||||
|  |       )} | ||||||
|  |     </SimpleGrid> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										108
									
								
								src/frontend/src/pages/part/pricing/SaleHistoryPanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/frontend/src/pages/part/pricing/SaleHistoryPanel.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | import { t } from '@lingui/macro'; | ||||||
|  | import { SimpleGrid } from '@mantine/core'; | ||||||
|  | import { ReactNode, useMemo } from 'react'; | ||||||
|  | import { | ||||||
|  |   Bar, | ||||||
|  |   BarChart, | ||||||
|  |   Legend, | ||||||
|  |   ResponsiveContainer, | ||||||
|  |   Tooltip, | ||||||
|  |   XAxis, | ||||||
|  |   YAxis | ||||||
|  | } from 'recharts'; | ||||||
|  |  | ||||||
|  | import { CHART_COLORS } from '../../../components/charts/colors'; | ||||||
|  | import { formatCurrency, renderDate } from '../../../defaults/formatters'; | ||||||
|  | import { ApiEndpoints } from '../../../enums/ApiEndpoints'; | ||||||
|  | import { useTable } from '../../../hooks/UseTable'; | ||||||
|  | import { apiUrl } from '../../../states/ApiState'; | ||||||
|  | import { TableColumn } from '../../../tables/Column'; | ||||||
|  | import { InvenTreeTable } from '../../../tables/InvenTreeTable'; | ||||||
|  | import { NoPricingData } from './PricingPanel'; | ||||||
|  |  | ||||||
|  | export default function SaleHistoryPanel({ part }: { part: any }): ReactNode { | ||||||
|  |   const table = useTable('pricing-sale-history'); | ||||||
|  |  | ||||||
|  |   const columns: TableColumn[] = useMemo(() => { | ||||||
|  |     return [ | ||||||
|  |       { | ||||||
|  |         accessor: 'order', | ||||||
|  |         title: t`Sale Order`, | ||||||
|  |         render: (record: any) => record?.order_detail?.reference, | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: false | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         accessor: 'customer', | ||||||
|  |         title: t`Customer`, | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: true, | ||||||
|  |         render: (record: any) => record?.customer_detail?.name | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         accessor: 'shipment_date', | ||||||
|  |         title: t`Date`, | ||||||
|  |         sortable: false, | ||||||
|  |         switchable: true, | ||||||
|  |         render: (record: any) => renderDate(record.order_detail.shipment_date) | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         accessor: 'sale_price', | ||||||
|  |         title: t`Sale Price`, | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: false, | ||||||
|  |         render: (record: any) => { | ||||||
|  |           return formatCurrency(record.sale_price, { | ||||||
|  |             currency: record.sale_price_currency | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     ]; | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   const saleHistoryData = useMemo(() => { | ||||||
|  |     return table.records.map((record: any) => { | ||||||
|  |       return { | ||||||
|  |         name: record.order_detail.reference, | ||||||
|  |         sale_price: record.sale_price | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |   }, [table.records]); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <SimpleGrid cols={2}> | ||||||
|  |       <InvenTreeTable | ||||||
|  |         tableState={table} | ||||||
|  |         url={apiUrl(ApiEndpoints.sales_order_line_list)} | ||||||
|  |         columns={columns} | ||||||
|  |         props={{ | ||||||
|  |           params: { | ||||||
|  |             part: part.pk, | ||||||
|  |             part_detail: true, | ||||||
|  |             order_detail: true, | ||||||
|  |             customer_detail: true, | ||||||
|  |             has_pricing: true, | ||||||
|  |             order_complete: true | ||||||
|  |           } | ||||||
|  |         }} | ||||||
|  |       /> | ||||||
|  |       {saleHistoryData.length > 0 ? ( | ||||||
|  |         <ResponsiveContainer width="100%" height={500}> | ||||||
|  |           <BarChart data={saleHistoryData}> | ||||||
|  |             <XAxis dataKey="name" /> | ||||||
|  |             <YAxis /> | ||||||
|  |             <Tooltip /> | ||||||
|  |             <Legend /> | ||||||
|  |             <Bar | ||||||
|  |               dataKey="sale_price" | ||||||
|  |               fill={CHART_COLORS[0]} | ||||||
|  |               label={t`Sale Price`} | ||||||
|  |             /> | ||||||
|  |           </BarChart> | ||||||
|  |         </ResponsiveContainer> | ||||||
|  |       ) : ( | ||||||
|  |         <NoPricingData /> | ||||||
|  |       )} | ||||||
|  |     </SimpleGrid> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										80
									
								
								src/frontend/src/pages/part/pricing/SupplierPricingPanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/frontend/src/pages/part/pricing/SupplierPricingPanel.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | |||||||
|  | import { t } from '@lingui/macro'; | ||||||
|  | import { SimpleGrid } from '@mantine/core'; | ||||||
|  | import { useMemo } from 'react'; | ||||||
|  | import { | ||||||
|  |   Bar, | ||||||
|  |   BarChart, | ||||||
|  |   ResponsiveContainer, | ||||||
|  |   Tooltip, | ||||||
|  |   XAxis, | ||||||
|  |   YAxis | ||||||
|  | } from 'recharts'; | ||||||
|  |  | ||||||
|  | import { CHART_COLORS } from '../../../components/charts/colors'; | ||||||
|  | import { ApiEndpoints } from '../../../enums/ApiEndpoints'; | ||||||
|  | import { useTable } from '../../../hooks/UseTable'; | ||||||
|  | import { apiUrl } from '../../../states/ApiState'; | ||||||
|  | import { TableColumn } from '../../../tables/Column'; | ||||||
|  | import { InvenTreeTable } from '../../../tables/InvenTreeTable'; | ||||||
|  | import { | ||||||
|  |   SupplierPriceBreakColumns, | ||||||
|  |   calculateSupplierPartUnitPrice | ||||||
|  | } from '../../../tables/purchasing/SupplierPriceBreakTable'; | ||||||
|  | import { NoPricingData } from './PricingPanel'; | ||||||
|  |  | ||||||
|  | export default function SupplierPricingPanel({ part }: { part: any }) { | ||||||
|  |   const table = useTable('pricing-supplier'); | ||||||
|  |  | ||||||
|  |   const columns: TableColumn[] = useMemo(() => { | ||||||
|  |     return SupplierPriceBreakColumns(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   const supplierPricingData = useMemo(() => { | ||||||
|  |     return table.records.map((record: any) => { | ||||||
|  |       return { | ||||||
|  |         quantity: record.quantity, | ||||||
|  |         supplier_price: record.price, | ||||||
|  |         unit_price: calculateSupplierPartUnitPrice(record), | ||||||
|  |         name: record.part_detail?.SKU | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |   }, [table.records]); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <SimpleGrid cols={2}> | ||||||
|  |       <InvenTreeTable | ||||||
|  |         url={apiUrl(ApiEndpoints.supplier_part_pricing_list)} | ||||||
|  |         columns={columns} | ||||||
|  |         tableState={table} | ||||||
|  |         props={{ | ||||||
|  |           params: { | ||||||
|  |             base_part: part.pk, | ||||||
|  |             supplier_detail: true, | ||||||
|  |             part_detail: true | ||||||
|  |           } | ||||||
|  |         }} | ||||||
|  |       /> | ||||||
|  |       {supplierPricingData.length > 0 ? ( | ||||||
|  |         <ResponsiveContainer width="100%" height={500}> | ||||||
|  |           <BarChart data={supplierPricingData}> | ||||||
|  |             <XAxis dataKey="name" /> | ||||||
|  |             <YAxis /> | ||||||
|  |             <Tooltip /> | ||||||
|  |             <Bar | ||||||
|  |               dataKey="unit_price" | ||||||
|  |               fill={CHART_COLORS[0]} | ||||||
|  |               label={t`Unit Price`} | ||||||
|  |             /> | ||||||
|  |             <Bar | ||||||
|  |               dataKey="supplier_price" | ||||||
|  |               fill="#82ca9d" | ||||||
|  |               label={t`Supplier Price`} | ||||||
|  |             /> | ||||||
|  |           </BarChart> | ||||||
|  |         </ResponsiveContainer> | ||||||
|  |       ) : ( | ||||||
|  |         <NoPricingData /> | ||||||
|  |       )} | ||||||
|  |     </SimpleGrid> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										121
									
								
								src/frontend/src/pages/part/pricing/VariantPricingPanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/frontend/src/pages/part/pricing/VariantPricingPanel.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | |||||||
|  | import { t } from '@lingui/macro'; | ||||||
|  | import { SimpleGrid, Stack } from '@mantine/core'; | ||||||
|  | import { ReactNode, useMemo } from 'react'; | ||||||
|  | import { | ||||||
|  |   Bar, | ||||||
|  |   BarChart, | ||||||
|  |   Legend, | ||||||
|  |   ResponsiveContainer, | ||||||
|  |   Tooltip, | ||||||
|  |   XAxis, | ||||||
|  |   YAxis | ||||||
|  | } from 'recharts'; | ||||||
|  |  | ||||||
|  | import { CHART_COLORS } from '../../../components/charts/colors'; | ||||||
|  | import { formatCurrency } from '../../../defaults/formatters'; | ||||||
|  | import { ApiEndpoints } from '../../../enums/ApiEndpoints'; | ||||||
|  | import { useTable } from '../../../hooks/UseTable'; | ||||||
|  | import { apiUrl } from '../../../states/ApiState'; | ||||||
|  | import { TableColumn } from '../../../tables/Column'; | ||||||
|  | import { DateColumn, PartColumn } from '../../../tables/ColumnRenderers'; | ||||||
|  | import { InvenTreeTable } from '../../../tables/InvenTreeTable'; | ||||||
|  | import { NoPricingData } from './PricingPanel'; | ||||||
|  |  | ||||||
|  | export default function VariantPricingPanel({ | ||||||
|  |   part, | ||||||
|  |   pricing | ||||||
|  | }: { | ||||||
|  |   part: any; | ||||||
|  |   pricing: any; | ||||||
|  | }): ReactNode { | ||||||
|  |   const table = useTable('pricing-variants'); | ||||||
|  |  | ||||||
|  |   const columns: TableColumn[] = useMemo(() => { | ||||||
|  |     return [ | ||||||
|  |       { | ||||||
|  |         accessor: 'name', | ||||||
|  |         title: t`Variant Part`, | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: false, | ||||||
|  |         render: (record: any) => PartColumn(record) | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         accessor: 'pricing_min', | ||||||
|  |         title: t`Minimum Price`, | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: false, | ||||||
|  |         render: (record: any) => | ||||||
|  |           formatCurrency(record.pricing_min, { currency: pricing?.currency }) | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         accessor: 'pricing_max', | ||||||
|  |         title: t`Maximum Price`, | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: false, | ||||||
|  |         render: (record: any) => | ||||||
|  |           formatCurrency(record.pricing_max, { currency: pricing?.currency }) | ||||||
|  |       }, | ||||||
|  |       DateColumn({ | ||||||
|  |         accessor: 'pricing_updated', | ||||||
|  |         title: t`Updated`, | ||||||
|  |         sortable: true, | ||||||
|  |         switchable: true | ||||||
|  |       }) | ||||||
|  |     ]; | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   // Calculate pricing data for the part variants | ||||||
|  |   const variantPricingData: any[] = useMemo(() => { | ||||||
|  |     const pricing = table.records.map((variant: any) => { | ||||||
|  |       return { | ||||||
|  |         part: variant, | ||||||
|  |         name: variant.full_name, | ||||||
|  |         pmin: variant.pricing_min ?? variant.pricing_max ?? 0, | ||||||
|  |         pmax: variant.pricing_max ?? variant.pricing_min ?? 0 | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return pricing; | ||||||
|  |   }, [table.records]); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Stack spacing="xs"> | ||||||
|  |       <SimpleGrid cols={2}> | ||||||
|  |         <InvenTreeTable | ||||||
|  |           tableState={table} | ||||||
|  |           url={apiUrl(ApiEndpoints.part_list)} | ||||||
|  |           columns={columns} | ||||||
|  |           props={{ | ||||||
|  |             params: { | ||||||
|  |               ancestor: part?.pk, | ||||||
|  |               has_pricing: true | ||||||
|  |             }, | ||||||
|  |             enablePagination: false | ||||||
|  |           }} | ||||||
|  |         /> | ||||||
|  |         {variantPricingData.length > 0 ? ( | ||||||
|  |           <ResponsiveContainer width="100%" height={500}> | ||||||
|  |             <BarChart data={variantPricingData}> | ||||||
|  |               <XAxis dataKey="name" /> | ||||||
|  |               <YAxis /> | ||||||
|  |               <Tooltip /> | ||||||
|  |               <Legend /> | ||||||
|  |               <Bar | ||||||
|  |                 dataKey="pmin" | ||||||
|  |                 fill={CHART_COLORS[0]} | ||||||
|  |                 label={t`Minimum Price`} | ||||||
|  |               /> | ||||||
|  |               <Bar | ||||||
|  |                 dataKey="pmax" | ||||||
|  |                 fill={CHART_COLORS[1]} | ||||||
|  |                 label={t`Maximum Price`} | ||||||
|  |               /> | ||||||
|  |             </BarChart> | ||||||
|  |           </ResponsiveContainer> | ||||||
|  |         ) : ( | ||||||
|  |           <NoPricingData /> | ||||||
|  |         )} | ||||||
|  |       </SimpleGrid> | ||||||
|  |     </Stack> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -8,7 +8,7 @@ export type TableColumn<T = any> = { | |||||||
|   sortable?: boolean; // Whether the column is sortable |   sortable?: boolean; // Whether the column is sortable | ||||||
|   switchable?: boolean; // Whether the column is switchable |   switchable?: boolean; // Whether the column is switchable | ||||||
|   hidden?: boolean; // Whether the column is hidden |   hidden?: boolean; // Whether the column is hidden | ||||||
|   render?: (record: T) => any; // A custom render function |   render?: (record: T, index?: number) => any; // A custom render function | ||||||
|   filter?: any; // A custom filter function |   filter?: any; // A custom filter function | ||||||
|   filtering?: boolean; // Whether the column is filterable |   filtering?: boolean; // Whether the column is filterable | ||||||
|   width?: number; // The width of the column |   width?: number; // The width of the column | ||||||
|   | |||||||
| @@ -148,12 +148,23 @@ export function ResponsibleColumn(): TableColumn { | |||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function DateColumn(): TableColumn { | export function DateColumn({ | ||||||
|  |   accessor, | ||||||
|  |   sortable, | ||||||
|  |   switchable, | ||||||
|  |   title | ||||||
|  | }: { | ||||||
|  |   accessor?: string; | ||||||
|  |   sortable?: boolean; | ||||||
|  |   switchable?: boolean; | ||||||
|  |   title?: string; | ||||||
|  | }): TableColumn { | ||||||
|   return { |   return { | ||||||
|     accessor: 'date', |     accessor: accessor ?? 'date', | ||||||
|     sortable: true, |     sortable: sortable ?? true, | ||||||
|     title: t`Date`, |     title: title ?? t`Date`, | ||||||
|     render: (record: any) => renderDate(record.date) |     switchable: switchable, | ||||||
|  |     render: (record: any) => renderDate(record[accessor ?? 'date']) | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ import { modals } from '@mantine/modals'; | |||||||
| import { showNotification } from '@mantine/notifications'; | import { showNotification } from '@mantine/notifications'; | ||||||
| import { IconFilter, IconRefresh, IconTrash } from '@tabler/icons-react'; | import { IconFilter, IconRefresh, IconTrash } from '@tabler/icons-react'; | ||||||
| import { IconBarcode, IconPrinter } from '@tabler/icons-react'; | import { IconBarcode, IconPrinter } from '@tabler/icons-react'; | ||||||
| import { dataTagSymbol, useQuery } from '@tanstack/react-query'; | import { useQuery } from '@tanstack/react-query'; | ||||||
| import { | import { | ||||||
|   DataTable, |   DataTable, | ||||||
|   DataTableCellClickHandler, |   DataTableCellClickHandler, | ||||||
| @@ -91,6 +91,7 @@ export type InvenTreeTableProps<T = any> = { | |||||||
|   onRowClick?: (record: T, index: number, event: any) => void; |   onRowClick?: (record: T, index: number, event: any) => void; | ||||||
|   onCellClick?: DataTableCellClickHandler<T>; |   onCellClick?: DataTableCellClickHandler<T>; | ||||||
|   modelType?: ModelType; |   modelType?: ModelType; | ||||||
|  |   rowStyle?: (record: T, index: number) => any; | ||||||
|   modelField?: string; |   modelField?: string; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -152,6 +153,7 @@ export function InvenTreeTable<T = any>({ | |||||||
|     queryKey: ['options', url, tableState.tableKey], |     queryKey: ['options', url, tableState.tableKey], | ||||||
|     retry: 3, |     retry: 3, | ||||||
|     refetchOnMount: true, |     refetchOnMount: true, | ||||||
|  |     refetchOnWindowFocus: false, | ||||||
|     queryFn: async () => { |     queryFn: async () => { | ||||||
|       return api |       return api | ||||||
|         .options(url, { |         .options(url, { | ||||||
| @@ -655,6 +657,7 @@ export function InvenTreeTable<T = any>({ | |||||||
|               tableProps.enableSelection ? onSelectedRecordsChange : undefined |               tableProps.enableSelection ? onSelectedRecordsChange : undefined | ||||||
|             } |             } | ||||||
|             rowExpansion={tableProps.rowExpansion} |             rowExpansion={tableProps.rowExpansion} | ||||||
|  |             rowStyle={tableProps.rowStyle} | ||||||
|             fetching={isFetching} |             fetching={isFetching} | ||||||
|             noRecordsText={missingRecordsText} |             noRecordsText={missingRecordsText} | ||||||
|             records={tableState.records} |             records={tableState.records} | ||||||
|   | |||||||
							
								
								
									
										222
									
								
								src/frontend/src/tables/purchasing/SupplierPriceBreakTable.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								src/frontend/src/tables/purchasing/SupplierPriceBreakTable.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,222 @@ | |||||||
|  | import { t } from '@lingui/macro'; | ||||||
|  | import { Anchor, Group, Text } from '@mantine/core'; | ||||||
|  | import { useCallback, useMemo, useState } from 'react'; | ||||||
|  |  | ||||||
|  | import { AddItemButton } from '../../components/buttons/AddItemButton'; | ||||||
|  | import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField'; | ||||||
|  | import { Thumbnail } from '../../components/images/Thumbnail'; | ||||||
|  | import { formatCurrency } from '../../defaults/formatters'; | ||||||
|  | import { ApiEndpoints } from '../../enums/ApiEndpoints'; | ||||||
|  | import { ModelType } from '../../enums/ModelType'; | ||||||
|  | import { UserRoles } from '../../enums/Roles'; | ||||||
|  | import { getDetailUrl } from '../../functions/urls'; | ||||||
|  | import { | ||||||
|  |   useCreateApiFormModal, | ||||||
|  |   useDeleteApiFormModal, | ||||||
|  |   useEditApiFormModal | ||||||
|  | } from '../../hooks/UseForm'; | ||||||
|  | import { useTable } from '../../hooks/UseTable'; | ||||||
|  | import { apiUrl } from '../../states/ApiState'; | ||||||
|  | import { useUserState } from '../../states/UserState'; | ||||||
|  | import { TableColumn } from '../Column'; | ||||||
|  | import { InvenTreeTable } from '../InvenTreeTable'; | ||||||
|  | import { RowDeleteAction, RowEditAction } from '../RowActions'; | ||||||
|  |  | ||||||
|  | export function calculateSupplierPartUnitPrice(record: any) { | ||||||
|  |   let pack_quantity = record?.part_detail?.pack_quantity_native ?? 1; | ||||||
|  |   let unit_price = record.price / pack_quantity; | ||||||
|  |  | ||||||
|  |   return unit_price; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function SupplierPriceBreakColumns(): TableColumn[] { | ||||||
|  |   return [ | ||||||
|  |     { | ||||||
|  |       accessor: 'supplier', | ||||||
|  |       title: t`Supplier`, | ||||||
|  |       sortable: true, | ||||||
|  |       switchable: true, | ||||||
|  |       render: (record: any) => { | ||||||
|  |         return ( | ||||||
|  |           <Group spacing="xs" noWrap> | ||||||
|  |             <Thumbnail | ||||||
|  |               src={ | ||||||
|  |                 record?.supplier_detail?.thumbnail ?? | ||||||
|  |                 record?.supplier_detail?.image | ||||||
|  |               } | ||||||
|  |               alt={record?.supplier_detail?.name} | ||||||
|  |               size={24} | ||||||
|  |             /> | ||||||
|  |             <Text>{record.supplier_detail?.name}</Text> | ||||||
|  |           </Group> | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       accessor: 'part_detail.SKU', | ||||||
|  |       title: t`SKU`, | ||||||
|  |       ordering: 'SKU', | ||||||
|  |       sortable: true, | ||||||
|  |       switchable: false, | ||||||
|  |       render: (record: any) => { | ||||||
|  |         return ( | ||||||
|  |           <Anchor | ||||||
|  |             href={getDetailUrl(ModelType.supplierpart, record.part_detail.pk)} | ||||||
|  |           > | ||||||
|  |             {record.part_detail.SKU} | ||||||
|  |           </Anchor> | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       accessor: 'quantity', | ||||||
|  |       title: t`Quantity`, | ||||||
|  |       sortable: true, | ||||||
|  |       switchable: false | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       accessor: 'price', | ||||||
|  |       title: t`Supplier Price`, | ||||||
|  |       render: (record: any) => | ||||||
|  |         formatCurrency(record.price, { currency: record.price_currency }), | ||||||
|  |       sortable: true, | ||||||
|  |       switchable: false | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       accessor: 'unit_price', | ||||||
|  |       ordering: 'price', | ||||||
|  |       title: t`Unit Price`, | ||||||
|  |       sortable: true, | ||||||
|  |       switchable: true, | ||||||
|  |       render: (record: any) => { | ||||||
|  |         let units = record.part_detail?.pack_quantity; | ||||||
|  |  | ||||||
|  |         let price = formatCurrency(calculateSupplierPartUnitPrice(record), { | ||||||
|  |           currency: record.price_currency | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         return ( | ||||||
|  |           <Group position="apart" spacing="xs" grow> | ||||||
|  |             <Text>{price}</Text> | ||||||
|  |             {units && <Text size="xs">[{units}]</Text>} | ||||||
|  |           </Group> | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default function SupplierPriceBreakTable({ | ||||||
|  |   supplierPartId | ||||||
|  | }: { | ||||||
|  |   supplierPartId: number; | ||||||
|  | }) { | ||||||
|  |   const table = useTable('supplierpricebreaks'); | ||||||
|  |  | ||||||
|  |   const user = useUserState(); | ||||||
|  |  | ||||||
|  |   const columns: TableColumn[] = useMemo(() => { | ||||||
|  |     return SupplierPriceBreakColumns(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   const supplierPriceBreakFields: ApiFormFieldSet = useMemo(() => { | ||||||
|  |     return { | ||||||
|  |       part: { | ||||||
|  |         hidden: false, | ||||||
|  |         disabled: true | ||||||
|  |       }, | ||||||
|  |       quantity: {}, | ||||||
|  |       price: {}, | ||||||
|  |       price_currency: {} | ||||||
|  |     }; | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   const [selectedPriceBreak, setSelectedPriceBreak] = useState<number>(0); | ||||||
|  |  | ||||||
|  |   const newPriceBreak = useCreateApiFormModal({ | ||||||
|  |     url: apiUrl(ApiEndpoints.supplier_part_pricing_list), | ||||||
|  |     title: t`Add Price Break`, | ||||||
|  |     fields: supplierPriceBreakFields, | ||||||
|  |     initialData: { | ||||||
|  |       part: supplierPartId | ||||||
|  |     }, | ||||||
|  |     onFormSuccess: (data: any) => { | ||||||
|  |       table.refreshTable(); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const editPriceBreak = useEditApiFormModal({ | ||||||
|  |     url: apiUrl(ApiEndpoints.supplier_part_pricing_list), | ||||||
|  |     pk: selectedPriceBreak, | ||||||
|  |     title: t`Edit Price Break`, | ||||||
|  |     fields: supplierPriceBreakFields, | ||||||
|  |     onFormSuccess: (data: any) => { | ||||||
|  |       table.refreshTable(); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const deletePriceBreak = useDeleteApiFormModal({ | ||||||
|  |     url: apiUrl(ApiEndpoints.supplier_part_pricing_list), | ||||||
|  |     pk: selectedPriceBreak, | ||||||
|  |     title: t`Delete Price Break`, | ||||||
|  |     onFormSuccess: () => { | ||||||
|  |       table.refreshTable(); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const tableActions = useMemo(() => { | ||||||
|  |     return [ | ||||||
|  |       <AddItemButton | ||||||
|  |         tooltip={t`Add Price Break`} | ||||||
|  |         onClick={() => { | ||||||
|  |           newPriceBreak.open(); | ||||||
|  |         }} | ||||||
|  |         hidden={!user.hasAddRole(UserRoles.part)} | ||||||
|  |       /> | ||||||
|  |     ]; | ||||||
|  |   }, [user]); | ||||||
|  |  | ||||||
|  |   const rowActions = useCallback( | ||||||
|  |     (record: any) => { | ||||||
|  |       return [ | ||||||
|  |         RowEditAction({ | ||||||
|  |           hidden: !user.hasChangeRole(UserRoles.purchase_order), | ||||||
|  |           onClick: () => { | ||||||
|  |             setSelectedPriceBreak(record.pk); | ||||||
|  |             editPriceBreak.open(); | ||||||
|  |           } | ||||||
|  |         }), | ||||||
|  |         RowDeleteAction({ | ||||||
|  |           hidden: !user.hasDeleteRole(UserRoles.purchase_order), | ||||||
|  |           onClick: () => { | ||||||
|  |             setSelectedPriceBreak(record.pk); | ||||||
|  |             deletePriceBreak.open(); | ||||||
|  |           } | ||||||
|  |         }) | ||||||
|  |       ]; | ||||||
|  |     }, | ||||||
|  |     [user] | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       {newPriceBreak.modal} | ||||||
|  |       {editPriceBreak.modal} | ||||||
|  |       {deletePriceBreak.modal} | ||||||
|  |       <InvenTreeTable | ||||||
|  |         url={apiUrl(ApiEndpoints.supplier_part_pricing_list)} | ||||||
|  |         columns={columns} | ||||||
|  |         tableState={table} | ||||||
|  |         props={{ | ||||||
|  |           params: { | ||||||
|  |             part: supplierPartId, | ||||||
|  |             part_detail: true, | ||||||
|  |             supplier_detail: true | ||||||
|  |           }, | ||||||
|  |           tableActions: tableActions, | ||||||
|  |           rowActions: rowActions | ||||||
|  |         }} | ||||||
|  |       /> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -342,11 +342,19 @@ function stockItemTableFilters(): TableFilter[] { | |||||||
| /* | /* | ||||||
|  * Load a table of stock items |  * Load a table of stock items | ||||||
|  */ |  */ | ||||||
| export function StockItemTable({ params = {} }: { params?: any }) { | export function StockItemTable({ | ||||||
|  |   params = {}, | ||||||
|  |   allowAdd = true, | ||||||
|  |   tableName = 'stockitems' | ||||||
|  | }: { | ||||||
|  |   params?: any; | ||||||
|  |   allowAdd?: boolean; | ||||||
|  |   tableName?: string; | ||||||
|  | }) { | ||||||
|   let tableColumns = useMemo(() => stockItemTableColumns(), []); |   let tableColumns = useMemo(() => stockItemTableColumns(), []); | ||||||
|   let tableFilters = useMemo(() => stockItemTableFilters(), []); |   let tableFilters = useMemo(() => stockItemTableFilters(), []); | ||||||
|  |  | ||||||
|   const table = useTable('stockitems'); |   const table = useTable(tableName); | ||||||
|   const user = useUserState(); |   const user = useUserState(); | ||||||
|  |  | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
| @@ -482,7 +490,7 @@ export function StockItemTable({ params = {} }: { params?: any }) { | |||||||
|         ]} |         ]} | ||||||
|       />, |       />, | ||||||
|       <AddItemButton |       <AddItemButton | ||||||
|         hidden={!user.hasAddRole(UserRoles.stock)} |         hidden={!allowAdd || !user.hasAddRole(UserRoles.stock)} | ||||||
|         tooltip={t`Add Stock Item`} |         tooltip={t`Add Stock Item`} | ||||||
|         onClick={() => newStockItem.open()} |         onClick={() => newStockItem.open()} | ||||||
|       /> |       /> | ||||||
|   | |||||||
| @@ -1520,6 +1520,57 @@ | |||||||
|   dependencies: |   dependencies: | ||||||
|     "@types/tern" "*" |     "@types/tern" "*" | ||||||
|  |  | ||||||
|  | "@types/d3-array@^3.0.3": | ||||||
|  |   version "3.2.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" | ||||||
|  |   integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== | ||||||
|  |  | ||||||
|  | "@types/d3-color@*": | ||||||
|  |   version "3.1.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" | ||||||
|  |   integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== | ||||||
|  |  | ||||||
|  | "@types/d3-ease@^3.0.0": | ||||||
|  |   version "3.0.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" | ||||||
|  |   integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== | ||||||
|  |  | ||||||
|  | "@types/d3-interpolate@^3.0.1": | ||||||
|  |   version "3.0.4" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" | ||||||
|  |   integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3-color" "*" | ||||||
|  |  | ||||||
|  | "@types/d3-path@*": | ||||||
|  |   version "3.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a" | ||||||
|  |   integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ== | ||||||
|  |  | ||||||
|  | "@types/d3-scale@^4.0.2": | ||||||
|  |   version "4.0.8" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" | ||||||
|  |   integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3-time" "*" | ||||||
|  |  | ||||||
|  | "@types/d3-shape@^3.1.0": | ||||||
|  |   version "3.1.6" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72" | ||||||
|  |   integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3-path" "*" | ||||||
|  |  | ||||||
|  | "@types/d3-time@*", "@types/d3-time@^3.0.0": | ||||||
|  |   version "3.0.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" | ||||||
|  |   integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== | ||||||
|  |  | ||||||
|  | "@types/d3-timer@^3.0.0": | ||||||
|  |   version "3.0.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" | ||||||
|  |   integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== | ||||||
|  |  | ||||||
| "@types/estree@*", "@types/estree@1.0.5": | "@types/estree@*", "@types/estree@1.0.5": | ||||||
|   version "1.0.5" |   version "1.0.5" | ||||||
|   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" |   resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" | ||||||
| @@ -2162,6 +2213,77 @@ csstype@^3.0.2: | |||||||
|   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" |   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" | ||||||
|   integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== |   integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== | ||||||
|  |  | ||||||
|  | "d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6: | ||||||
|  |   version "3.2.4" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" | ||||||
|  |   integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== | ||||||
|  |   dependencies: | ||||||
|  |     internmap "1 - 2" | ||||||
|  |  | ||||||
|  | "d3-color@1 - 3": | ||||||
|  |   version "3.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" | ||||||
|  |   integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== | ||||||
|  |  | ||||||
|  | d3-ease@^3.0.1: | ||||||
|  |   version "3.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" | ||||||
|  |   integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== | ||||||
|  |  | ||||||
|  | "d3-format@1 - 3": | ||||||
|  |   version "3.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" | ||||||
|  |   integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== | ||||||
|  |  | ||||||
|  | "d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: | ||||||
|  |   version "3.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" | ||||||
|  |   integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== | ||||||
|  |   dependencies: | ||||||
|  |     d3-color "1 - 3" | ||||||
|  |  | ||||||
|  | d3-path@^3.1.0: | ||||||
|  |   version "3.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" | ||||||
|  |   integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== | ||||||
|  |  | ||||||
|  | d3-scale@^4.0.2: | ||||||
|  |   version "4.0.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" | ||||||
|  |   integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== | ||||||
|  |   dependencies: | ||||||
|  |     d3-array "2.10.0 - 3" | ||||||
|  |     d3-format "1 - 3" | ||||||
|  |     d3-interpolate "1.2.0 - 3" | ||||||
|  |     d3-time "2.1.1 - 3" | ||||||
|  |     d3-time-format "2 - 4" | ||||||
|  |  | ||||||
|  | d3-shape@^3.1.0: | ||||||
|  |   version "3.2.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" | ||||||
|  |   integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== | ||||||
|  |   dependencies: | ||||||
|  |     d3-path "^3.1.0" | ||||||
|  |  | ||||||
|  | "d3-time-format@2 - 4": | ||||||
|  |   version "4.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" | ||||||
|  |   integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== | ||||||
|  |   dependencies: | ||||||
|  |     d3-time "1 - 3" | ||||||
|  |  | ||||||
|  | "d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: | ||||||
|  |   version "3.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" | ||||||
|  |   integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== | ||||||
|  |   dependencies: | ||||||
|  |     d3-array "2 - 3" | ||||||
|  |  | ||||||
|  | d3-timer@^3.0.1: | ||||||
|  |   version "3.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" | ||||||
|  |   integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== | ||||||
|  |  | ||||||
| date-fns@^3.6.0: | date-fns@^3.6.0: | ||||||
|   version "3.6.0" |   version "3.6.0" | ||||||
|   resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" |   resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" | ||||||
| @@ -2184,6 +2306,11 @@ decamelize@^1.2.0: | |||||||
|   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" |   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" | ||||||
|   integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== |   integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== | ||||||
|  |  | ||||||
|  | decimal.js-light@^2.4.1: | ||||||
|  |   version "2.5.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" | ||||||
|  |   integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== | ||||||
|  |  | ||||||
| default-require-extensions@^3.0.0: | default-require-extensions@^3.0.0: | ||||||
|   version "3.0.1" |   version "3.0.1" | ||||||
|   resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.1.tgz#bfae00feeaeada68c2ae256c62540f60b80625bd" |   resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.1.tgz#bfae00feeaeada68c2ae256c62540f60b80625bd" | ||||||
| @@ -2358,6 +2485,11 @@ esprima@^4.0.0: | |||||||
|   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" |   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" | ||||||
|   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== |   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== | ||||||
|  |  | ||||||
|  | eventemitter3@^4.0.1: | ||||||
|  |   version "4.0.7" | ||||||
|  |   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" | ||||||
|  |   integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== | ||||||
|  |  | ||||||
| external-editor@^3.0.3: | external-editor@^3.0.3: | ||||||
|   version "3.1.0" |   version "3.1.0" | ||||||
|   resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" |   resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" | ||||||
| @@ -2377,6 +2509,11 @@ fast-equals@^4.0.3: | |||||||
|   resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-4.0.3.tgz#72884cc805ec3c6679b99875f6b7654f39f0e8c7" |   resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-4.0.3.tgz#72884cc805ec3c6679b99875f6b7654f39f0e8c7" | ||||||
|   integrity sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg== |   integrity sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg== | ||||||
|  |  | ||||||
|  | fast-equals@^5.0.1: | ||||||
|  |   version "5.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.0.1.tgz#a4eefe3c5d1c0d021aeed0bc10ba5e0c12ee405d" | ||||||
|  |   integrity sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ== | ||||||
|  |  | ||||||
| figures@^3.0.0: | figures@^3.0.0: | ||||||
|   version "3.2.0" |   version "3.2.0" | ||||||
|   resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" |   resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" | ||||||
| @@ -2627,6 +2764,11 @@ inquirer@^7.3.3: | |||||||
|     strip-ansi "^6.0.0" |     strip-ansi "^6.0.0" | ||||||
|     through "^2.3.6" |     through "^2.3.6" | ||||||
|  |  | ||||||
|  | "internmap@1 - 2": | ||||||
|  |   version "2.0.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" | ||||||
|  |   integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== | ||||||
|  |  | ||||||
| invariant@^2.2.4: | invariant@^2.2.4: | ||||||
|   version "2.2.4" |   version "2.2.4" | ||||||
|   resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" |   resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" | ||||||
| @@ -3355,7 +3497,7 @@ react-hook-form@^7.51.2: | |||||||
|   resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.51.2.tgz#79f7f72ee217c5114ff831012d1a7ec344096e7f" |   resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.51.2.tgz#79f7f72ee217c5114ff831012d1a7ec344096e7f" | ||||||
|   integrity sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA== |   integrity sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA== | ||||||
|  |  | ||||||
| react-is@^16.13.1, react-is@^16.7.0: | react-is@^16.10.2, react-is@^16.13.1, react-is@^16.7.0: | ||||||
|   version "16.13.1" |   version "16.13.1" | ||||||
|   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" |   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" | ||||||
|   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== |   integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== | ||||||
| @@ -3434,6 +3576,15 @@ react-simplemde-editor@^5.2.0: | |||||||
|   dependencies: |   dependencies: | ||||||
|     "@types/codemirror" "~5.60.5" |     "@types/codemirror" "~5.60.5" | ||||||
|  |  | ||||||
|  | react-smooth@^4.0.0: | ||||||
|  |   version "4.0.1" | ||||||
|  |   resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-4.0.1.tgz#6200d8699bfe051ae40ba187988323b1449eab1a" | ||||||
|  |   integrity sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w== | ||||||
|  |   dependencies: | ||||||
|  |     fast-equals "^5.0.1" | ||||||
|  |     prop-types "^15.8.1" | ||||||
|  |     react-transition-group "^4.4.5" | ||||||
|  |  | ||||||
| react-style-singleton@^2.2.1: | react-style-singleton@^2.2.1: | ||||||
|   version "2.2.1" |   version "2.2.1" | ||||||
|   resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" |   resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" | ||||||
| @@ -3462,7 +3613,7 @@ react-transition-group@4.4.2: | |||||||
|     loose-envify "^1.4.0" |     loose-envify "^1.4.0" | ||||||
|     prop-types "^15.6.2" |     prop-types "^15.6.2" | ||||||
|  |  | ||||||
| react-transition-group@^4.3.0: | react-transition-group@^4.3.0, react-transition-group@^4.4.5: | ||||||
|   version "4.4.5" |   version "4.4.5" | ||||||
|   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" |   resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" | ||||||
|   integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== |   integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== | ||||||
| @@ -3495,6 +3646,27 @@ readdirp@~3.5.0: | |||||||
|   dependencies: |   dependencies: | ||||||
|     picomatch "^2.2.1" |     picomatch "^2.2.1" | ||||||
|  |  | ||||||
|  | recharts-scale@^0.4.4: | ||||||
|  |   version "0.4.5" | ||||||
|  |   resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9" | ||||||
|  |   integrity sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w== | ||||||
|  |   dependencies: | ||||||
|  |     decimal.js-light "^2.4.1" | ||||||
|  |  | ||||||
|  | recharts@^2.12.4: | ||||||
|  |   version "2.12.4" | ||||||
|  |   resolved "https://registry.yarnpkg.com/recharts/-/recharts-2.12.4.tgz#e560a57cd44ab554c99a0d93bdd58d059b309a2e" | ||||||
|  |   integrity sha512-dM4skmk4fDKEDjL9MNunxv6zcTxePGVEzRnLDXALRpfJ85JoQ0P0APJ/CoJlmnQI0gPjBlOkjzrwrfQrRST3KA== | ||||||
|  |   dependencies: | ||||||
|  |     clsx "^2.0.0" | ||||||
|  |     eventemitter3 "^4.0.1" | ||||||
|  |     lodash "^4.17.21" | ||||||
|  |     react-is "^16.10.2" | ||||||
|  |     react-smooth "^4.0.0" | ||||||
|  |     recharts-scale "^0.4.4" | ||||||
|  |     tiny-invariant "^1.3.1" | ||||||
|  |     victory-vendor "^36.6.8" | ||||||
|  |  | ||||||
| regenerator-runtime@^0.14.0: | regenerator-runtime@^0.14.0: | ||||||
|   version "0.14.1" |   version "0.14.1" | ||||||
|   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" |   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" | ||||||
| @@ -3847,6 +4019,11 @@ through@^2.3.6: | |||||||
|   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" |   resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" | ||||||
|   integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== |   integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== | ||||||
|  |  | ||||||
|  | tiny-invariant@^1.3.1: | ||||||
|  |   version "1.3.3" | ||||||
|  |   resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" | ||||||
|  |   integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== | ||||||
|  |  | ||||||
| tmp@^0.0.33: | tmp@^0.0.33: | ||||||
|   version "0.0.33" |   version "0.0.33" | ||||||
|   resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" |   resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" | ||||||
| @@ -3975,6 +4152,26 @@ uuid@^8.3.2: | |||||||
|   resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" |   resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" | ||||||
|   integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== |   integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== | ||||||
|  |  | ||||||
|  | victory-vendor@^36.6.8: | ||||||
|  |   version "36.9.2" | ||||||
|  |   resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801" | ||||||
|  |   integrity sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ== | ||||||
|  |   dependencies: | ||||||
|  |     "@types/d3-array" "^3.0.3" | ||||||
|  |     "@types/d3-ease" "^3.0.0" | ||||||
|  |     "@types/d3-interpolate" "^3.0.1" | ||||||
|  |     "@types/d3-scale" "^4.0.2" | ||||||
|  |     "@types/d3-shape" "^3.1.0" | ||||||
|  |     "@types/d3-time" "^3.0.0" | ||||||
|  |     "@types/d3-timer" "^3.0.0" | ||||||
|  |     d3-array "^3.1.6" | ||||||
|  |     d3-ease "^3.0.1" | ||||||
|  |     d3-interpolate "^3.0.1" | ||||||
|  |     d3-scale "^4.0.2" | ||||||
|  |     d3-shape "^3.1.0" | ||||||
|  |     d3-time "^3.0.0" | ||||||
|  |     d3-timer "^3.0.1" | ||||||
|  |  | ||||||
| vite-plugin-babel-macros@^1.0.6: | vite-plugin-babel-macros@^1.0.6: | ||||||
|   version "1.0.6" |   version "1.0.6" | ||||||
|   resolved "https://registry.yarnpkg.com/vite-plugin-babel-macros/-/vite-plugin-babel-macros-1.0.6.tgz#d05cee3c38c620ccb534e38f412fdd899a3365b5" |   resolved "https://registry.yarnpkg.com/vite-plugin-babel-macros/-/vite-plugin-babel-macros-1.0.6.tgz#d05cee3c38c620ccb534e38f412fdd899a3365b5" | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								tasks.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								tasks.py
									
									
									
									
									
								
							| @@ -313,7 +313,9 @@ def remove_mfa(c, mail=''): | |||||||
| def static(c, frontend=False): | def static(c, frontend=False): | ||||||
|     """Copies required static files to the STATIC_ROOT directory, as per Django requirements.""" |     """Copies required static files to the STATIC_ROOT directory, as per Django requirements.""" | ||||||
|     manage(c, 'prerender') |     manage(c, 'prerender') | ||||||
|  |  | ||||||
|     if frontend and node_available(): |     if frontend and node_available(): | ||||||
|  |         frontend_trans(c) | ||||||
|         frontend_build(c) |         frontend_build(c) | ||||||
|  |  | ||||||
|     print('Collecting static files...') |     print('Collecting static files...') | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user