mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +00:00 
			
		
		
		
	Feature: Supplier part pack size (#3644)
* Adds 'pack_size' field to SupplierPart model * Edit pack_size for SupplierPart via API * Display pack size in supplier part page template * Improve table ordering for SupplierPart table * Fix for API filtering - Need to use custom filter class * Adds functionality to duplicate an existing SupplierPart * Bump API version number * Display annotation of pack size in purchase order line item table * Display additional information in part purchase order table * Add UOM to purchase order table * Improve receive items functionality * Indicate quantity which will be received in modal form * Update the received quantity as the user changes the value * Take the pack_size into account when receiving line items * Take supplierpart pack size into account when receiving line items * Add "pack size" column to purchase order line item table * Tweak supplier part table * Update 'on_order' queryset annotation to take pack_size into account - May god have mercy on my soul * Adds a unit test to validate that the on_order queryset annotation is working as expected * Update Part.on_order method to take pack_size into account - Check in existing unit test also * Fix existing unit tests - Previous unit test was actually in error - Logic for calculating "on_order" was broked * More unit tests for receiving items against a purchase order * Allow pack_size < 1 * Display pack size when adding / editing PurchaseOrderLineItem * Fix bug in part purchase order table * Update part purchase order table again * Exclude notificationmessage when exporting dataset * Also display pack size when ordering parts from secondary form * javascript linting * Change user facing strings to "Pack Quantity"
This commit is contained in:
		| @@ -8,6 +8,7 @@ from django_filters.rest_framework import DjangoFilterBackend | ||||
| from rest_framework import filters | ||||
|  | ||||
| from InvenTree.api import AttachmentMixin, ListCreateDestroyAPIView | ||||
| from InvenTree.filters import InvenTreeOrderingFilter | ||||
| from InvenTree.helpers import str2bool | ||||
| from InvenTree.mixins import ListCreateAPI, RetrieveUpdateDestroyAPI | ||||
|  | ||||
| @@ -338,12 +339,30 @@ class SupplierPartList(ListCreateDestroyAPIView): | ||||
|     filter_backends = [ | ||||
|         DjangoFilterBackend, | ||||
|         filters.SearchFilter, | ||||
|         filters.OrderingFilter, | ||||
|         InvenTreeOrderingFilter, | ||||
|     ] | ||||
|  | ||||
|     filterset_fields = [ | ||||
|     ] | ||||
|  | ||||
|     ordering_fields = [ | ||||
|         'SKU', | ||||
|         'part', | ||||
|         'supplier', | ||||
|         'manufacturer', | ||||
|         'MPN', | ||||
|         'packaging', | ||||
|         'pack_size', | ||||
|         'in_stock', | ||||
|     ] | ||||
|  | ||||
|     ordering_field_aliases = { | ||||
|         'part': 'part__name', | ||||
|         'supplier': 'supplier__name', | ||||
|         'manufacturer': 'manufacturer_part__manufacturer__name', | ||||
|         'MPN': 'manufacturer_part__MPN', | ||||
|     } | ||||
|  | ||||
|     search_fields = [ | ||||
|         'SKU', | ||||
|         'supplier__name', | ||||
|   | ||||
							
								
								
									
										20
									
								
								InvenTree/company/migrations/0047_supplierpart_pack_size.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								InvenTree/company/migrations/0047_supplierpart_pack_size.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| # Generated by Django 3.2.15 on 2022-09-05 04:21 | ||||
|  | ||||
| import InvenTree.fields | ||||
| import django.core.validators | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('company', '0046_alter_company_image'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='supplierpart', | ||||
|             name='pack_size', | ||||
|             field=InvenTree.fields.RoundingDecimalField(decimal_places=5, default=1, help_text='Unit quantity supplied in a single pack', max_digits=15, validators=[django.core.validators.MinValueValidator(0.001)], verbose_name='Pack Quantity'), | ||||
|         ), | ||||
|     ] | ||||
| @@ -20,7 +20,7 @@ import InvenTree.fields | ||||
| import InvenTree.helpers | ||||
| import InvenTree.validators | ||||
| from common.settings import currency_code_default | ||||
| from InvenTree.fields import InvenTreeURLField | ||||
| from InvenTree.fields import InvenTreeURLField, RoundingDecimalField | ||||
| from InvenTree.models import InvenTreeAttachment | ||||
| from InvenTree.status_codes import PurchaseOrderStatus | ||||
|  | ||||
| @@ -406,6 +406,7 @@ class SupplierPart(models.Model): | ||||
|         multiple: Multiple that the part is provided in | ||||
|         lead_time: Supplier lead time | ||||
|         packaging: packaging that the part is supplied in, e.g. "Reel" | ||||
|         pack_size: Quantity of item supplied in a single pack (e.g. 30ml in a single tube) | ||||
|     """ | ||||
|  | ||||
|     objects = SupplierPartManager() | ||||
| @@ -527,6 +528,14 @@ class SupplierPart(models.Model): | ||||
|  | ||||
|     packaging = models.CharField(max_length=50, blank=True, null=True, verbose_name=_('Packaging'), help_text=_('Part packaging')) | ||||
|  | ||||
|     pack_size = RoundingDecimalField( | ||||
|         verbose_name=_('Pack Quantity'), | ||||
|         help_text=_('Unit quantity supplied in a single pack'), | ||||
|         default=1, | ||||
|         max_digits=15, decimal_places=5, | ||||
|         validators=[MinValueValidator(0.001)], | ||||
|     ) | ||||
|  | ||||
|     multiple = models.PositiveIntegerField(default=1, validators=[MinValueValidator(1)], verbose_name=_('multiple'), help_text=_('Order multiple')) | ||||
|  | ||||
|     # TODO - Reimplement lead-time as a charfield with special validation (pattern matching). | ||||
|   | ||||
| @@ -239,6 +239,8 @@ class SupplierPartSerializer(InvenTreeModelSerializer): | ||||
|  | ||||
|     pretty_name = serializers.CharField(read_only=True) | ||||
|  | ||||
|     pack_size = serializers.FloatField() | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         """Initialize this serializer with extra detail fields as required""" | ||||
|  | ||||
| @@ -273,6 +275,8 @@ class SupplierPartSerializer(InvenTreeModelSerializer): | ||||
|  | ||||
|     manufacturer_part_detail = ManufacturerPartSerializer(source='manufacturer_part', read_only=True) | ||||
|  | ||||
|     url = serializers.CharField(source='get_absolute_url', read_only=True) | ||||
|  | ||||
|     class Meta: | ||||
|         """Metaclass options.""" | ||||
|  | ||||
| @@ -291,12 +295,14 @@ class SupplierPartSerializer(InvenTreeModelSerializer): | ||||
|             'note', | ||||
|             'pk', | ||||
|             'packaging', | ||||
|             'pack_size', | ||||
|             'part', | ||||
|             'part_detail', | ||||
|             'pretty_name', | ||||
|             'SKU', | ||||
|             'supplier', | ||||
|             'supplier_detail', | ||||
|             'url', | ||||
|         ] | ||||
|  | ||||
|         read_only_fields = [ | ||||
|   | ||||
| @@ -49,6 +49,11 @@ | ||||
|             <span class='fas fa-edit icon-green'></span> {% trans "Edit Supplier Part" %} | ||||
|         </a></li> | ||||
|         {% endif %} | ||||
|         {% if roles.purchase_order.add %} | ||||
|         <li><a class='dropdown-item' href='#' id='duplicate-part' title='{% trans "Duplicate Supplier Part" %}'> | ||||
|             <span class='fas fa-clone'></span> {% trans "Duplicate Supplier Part" %} | ||||
|         </a></li> | ||||
|         {% endif %} | ||||
|         {% if roles.purchase_order.delete %} | ||||
|         <li><a class='dropdown-item' href='#' id='delete-part' title='{% trans "Delete Supplier Part" %}'> | ||||
|             <span class='fas fa-trash-alt icon-red'></span> {% trans "Delete Supplier  Part" %} | ||||
| @@ -140,6 +145,13 @@ src="{% static 'img/blank_image.png' %}" | ||||
|             <td>{{ part.packaging }}{% include "clip.html"%}</td> | ||||
|         </tr> | ||||
|         {% endif %} | ||||
|         {% if part.pack_size != 1.0 %} | ||||
|         <tr> | ||||
|             <td><span class='fas fa-box'></span></td> | ||||
|             <td>{% trans "Pack Quantity" %}</td> | ||||
|             <td>{% decimal part.pack_size %}{% if part.part.units %} {{ part.part.units }}{% endif %}</td> | ||||
|         </tr> | ||||
|         {% endif %} | ||||
|         {% if part.note %} | ||||
|         <tr> | ||||
|             <td><span class='fas fa-sticky-note'></span></td> | ||||
| @@ -386,6 +398,12 @@ $('#update-part-availability').click(function() { | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| $('#duplicate-part').click(function() { | ||||
|     duplicateSupplierPart({{ part.pk }}, { | ||||
|         follow: true | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| $('#edit-part').click(function () { | ||||
|  | ||||
|     editSupplierPart({{ part.pk }}, { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user