mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 07:05:41 +00:00 
			
		
		
		
	Merge pull request #2436 from SchrodingersGat/assign-to-customer
Assign to customer
This commit is contained in:
		@@ -19,6 +19,7 @@ def add_default_reference(apps, schema_editor):
 | 
				
			|||||||
        build.save()
 | 
					        build.save()
 | 
				
			||||||
        count += 1
 | 
					        count += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if count > 0:
 | 
				
			||||||
        print(f"\nUpdated build reference for {count} existing BuildOrder objects")
 | 
					        print(f"\nUpdated build reference for {count} existing BuildOrder objects")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,7 @@
 | 
				
			|||||||
  fields:
 | 
					  fields:
 | 
				
			||||||
    name: Zerg Corp
 | 
					    name: Zerg Corp
 | 
				
			||||||
    description: We eat the competition
 | 
					    description: We eat the competition
 | 
				
			||||||
 | 
					    is_customer: False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- model: company.company
 | 
					- model: company.company
 | 
				
			||||||
  pk: 4
 | 
					  pk: 4
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -163,6 +163,23 @@ class StockTransfer(StockAdjustView):
 | 
				
			|||||||
    serializer_class = StockSerializers.StockTransferSerializer
 | 
					    serializer_class = StockSerializers.StockTransferSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockAssign(generics.CreateAPIView):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    API endpoint for assigning stock to a particular customer
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    queryset = StockItem.objects.all()
 | 
				
			||||||
 | 
					    serializer_class = StockSerializers.StockAssignmentSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_serializer_context(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ctx = super().get_serializer_context()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ctx['request'] = self.request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return ctx
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockLocationList(generics.ListCreateAPIView):
 | 
					class StockLocationList(generics.ListCreateAPIView):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    API endpoint for list view of StockLocation objects:
 | 
					    API endpoint for list view of StockLocation objects:
 | 
				
			||||||
@@ -1174,6 +1191,7 @@ stock_api_urls = [
 | 
				
			|||||||
    url(r'^add/', StockAdd.as_view(), name='api-stock-add'),
 | 
					    url(r'^add/', StockAdd.as_view(), name='api-stock-add'),
 | 
				
			||||||
    url(r'^remove/', StockRemove.as_view(), name='api-stock-remove'),
 | 
					    url(r'^remove/', StockRemove.as_view(), name='api-stock-remove'),
 | 
				
			||||||
    url(r'^transfer/', StockTransfer.as_view(), name='api-stock-transfer'),
 | 
					    url(r'^transfer/', StockTransfer.as_view(), name='api-stock-transfer'),
 | 
				
			||||||
 | 
					    url(r'^assign/', StockAssign.as_view(), name='api-stock-assign'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # StockItemAttachment API endpoints
 | 
					    # StockItemAttachment API endpoints
 | 
				
			||||||
    url(r'^attachment/', include([
 | 
					    url(r'^attachment/', include([
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,20 +21,6 @@ from part.models import Part
 | 
				
			|||||||
from .models import StockLocation, StockItem, StockItemTracking
 | 
					from .models import StockLocation, StockItem, StockItemTracking
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AssignStockItemToCustomerForm(HelperForm):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    Form for manually assigning a StockItem to a Customer
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    TODO: This could be a simple API driven form!
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        model = StockItem
 | 
					 | 
				
			||||||
        fields = [
 | 
					 | 
				
			||||||
            'customer',
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ReturnStockItemForm(HelperForm):
 | 
					class ReturnStockItemForm(HelperForm):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Form for manually returning a StockItem into stock
 | 
					    Form for manually returning a StockItem into stock
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,6 +28,8 @@ from .models import StockItemTestResult
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import common.models
 | 
					import common.models
 | 
				
			||||||
from common.settings import currency_code_default, currency_code_mappings
 | 
					from common.settings import currency_code_default, currency_code_mappings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import company.models
 | 
				
			||||||
from company.serializers import SupplierPartSerializer
 | 
					from company.serializers import SupplierPartSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import InvenTree.helpers
 | 
					import InvenTree.helpers
 | 
				
			||||||
@@ -537,6 +539,127 @@ class StockTrackingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockAssignmentItemSerializer(serializers.Serializer):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Serializer for a single StockItem with in StockAssignment request.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Here, the particular StockItem is being assigned (manually) to a customer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Fields:
 | 
				
			||||||
 | 
					        - item: StockItem object
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            'item',
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    item = serializers.PrimaryKeyRelatedField(
 | 
				
			||||||
 | 
					        queryset=StockItem.objects.all(),
 | 
				
			||||||
 | 
					        many=False,
 | 
				
			||||||
 | 
					        allow_null=False,
 | 
				
			||||||
 | 
					        required=True,
 | 
				
			||||||
 | 
					        label=_('Stock Item'),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def validate_item(self, item):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # The item must currently be "in stock"
 | 
				
			||||||
 | 
					        if not item.in_stock:
 | 
				
			||||||
 | 
					            raise ValidationError(_("Item must be in stock"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # The base part must be "salable"
 | 
				
			||||||
 | 
					        if not item.part.salable:
 | 
				
			||||||
 | 
					            raise ValidationError(_("Part must be salable"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # The item must not be allocated to a sales order
 | 
				
			||||||
 | 
					        if item.sales_order_allocations.count() > 0:
 | 
				
			||||||
 | 
					            raise ValidationError(_("Item is allocated to a sales order"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # The item must not be allocated to a build order
 | 
				
			||||||
 | 
					        if item.allocations.count() > 0:
 | 
				
			||||||
 | 
					            raise ValidationError(_("Item is allocated to a build order"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return item
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockAssignmentSerializer(serializers.Serializer):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Serializer for assigning one (or more) stock items to a customer.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This is a manual assignment process, separate for (for example) a Sales Order
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					        fields = [
 | 
				
			||||||
 | 
					            'items',
 | 
				
			||||||
 | 
					            'customer',
 | 
				
			||||||
 | 
					            'notes',
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    items = StockAssignmentItemSerializer(
 | 
				
			||||||
 | 
					        many=True,
 | 
				
			||||||
 | 
					        required=True,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    customer = serializers.PrimaryKeyRelatedField(
 | 
				
			||||||
 | 
					        queryset=company.models.Company.objects.all(),
 | 
				
			||||||
 | 
					        many=False,
 | 
				
			||||||
 | 
					        allow_null=False,
 | 
				
			||||||
 | 
					        required=True,
 | 
				
			||||||
 | 
					        label=_('Customer'),
 | 
				
			||||||
 | 
					        help_text=_('Customer to assign stock items'),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def validate_customer(self, customer):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if customer and not customer.is_customer:
 | 
				
			||||||
 | 
					            raise ValidationError(_('Selected company is not a customer'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return customer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    notes = serializers.CharField(
 | 
				
			||||||
 | 
					        required=False,
 | 
				
			||||||
 | 
					        allow_blank=True,
 | 
				
			||||||
 | 
					        label=_('Notes'),
 | 
				
			||||||
 | 
					        help_text=_('Stock assignment notes'),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def validate(self, data):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        data = super().validate(data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        items = data.get('items', [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if len(items) == 0:
 | 
				
			||||||
 | 
					            raise ValidationError(_("A list of stock items must be provided"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def save(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        request = self.context['request']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        user = getattr(request, 'user', None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        data = self.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        items = data['items']
 | 
				
			||||||
 | 
					        customer = data['customer']
 | 
				
			||||||
 | 
					        notes = data.get('notes', '')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with transaction.atomic():
 | 
				
			||||||
 | 
					            for item in items:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                stock_item = item['item']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                stock_item.allocateToCustomer(
 | 
				
			||||||
 | 
					                    customer,
 | 
				
			||||||
 | 
					                    user=user,
 | 
				
			||||||
 | 
					                    notes=notes,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockAdjustmentItemSerializer(serializers.Serializer):
 | 
					class StockAdjustmentItemSerializer(serializers.Serializer):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Serializer for a single StockItem within a stock adjument request.
 | 
					    Serializer for a single StockItem within a stock adjument request.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -568,11 +568,19 @@ $("#stock-convert").click(function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% if item.in_stock %}
 | 
					{% if item.in_stock %}
 | 
				
			||||||
$("#stock-assign-to-customer").click(function() {
 | 
					$("#stock-assign-to-customer").click(function() {
 | 
				
			||||||
    launchModalForm("{% url 'stock-item-assign' item.id %}",
 | 
					
 | 
				
			||||||
 | 
					    inventreeGet('{% url "api-stock-detail" item.pk %}', {}, {
 | 
				
			||||||
 | 
					        success: function(response) {
 | 
				
			||||||
 | 
					            assignStockToCustomer(
 | 
				
			||||||
 | 
					                [response],
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
            reload: true,
 | 
					                    success: function() {
 | 
				
			||||||
 | 
					                        location.reload();
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$("#stock-move").click(function() {
 | 
					$("#stock-move").click(function() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,8 +16,9 @@ from InvenTree.status_codes import StockStatus
 | 
				
			|||||||
from InvenTree.api_tester import InvenTreeAPITestCase
 | 
					from InvenTree.api_tester import InvenTreeAPITestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from common.models import InvenTreeSetting
 | 
					from common.models import InvenTreeSetting
 | 
				
			||||||
 | 
					import company.models
 | 
				
			||||||
from .models import StockItem, StockLocation
 | 
					import part.models
 | 
				
			||||||
 | 
					from stock.models import StockItem, StockLocation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockAPITestCase(InvenTreeAPITestCase):
 | 
					class StockAPITestCase(InvenTreeAPITestCase):
 | 
				
			||||||
@@ -732,3 +733,112 @@ class StockTestResultTest(StockAPITestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            # Check that an attachment has been uploaded
 | 
					            # Check that an attachment has been uploaded
 | 
				
			||||||
            self.assertIsNotNone(response.data['attachment'])
 | 
					            self.assertIsNotNone(response.data['attachment'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StockAssignTest(StockAPITestCase):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Unit tests for the stock assignment API endpoint,
 | 
				
			||||||
 | 
					    where stock items are manually assigned to a customer
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    URL = reverse('api-stock-assign')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_invalid(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test with empty data
 | 
				
			||||||
 | 
					        response = self.post(
 | 
				
			||||||
 | 
					            self.URL,
 | 
				
			||||||
 | 
					            data={},
 | 
				
			||||||
 | 
					            expected_code=400,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertIn('This field is required', str(response.data['items']))
 | 
				
			||||||
 | 
					        self.assertIn('This field is required', str(response.data['customer']))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test with an invalid customer
 | 
				
			||||||
 | 
					        response = self.post(
 | 
				
			||||||
 | 
					            self.URL,
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                'customer': 999,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            expected_code=400,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertIn('object does not exist', str(response.data['customer']))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test with a company which is *not* a customer
 | 
				
			||||||
 | 
					        response = self.post(
 | 
				
			||||||
 | 
					            self.URL,
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                'customer': 3,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            expected_code=400,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertIn('company is not a customer', str(response.data['customer']))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Test with an empty items list
 | 
				
			||||||
 | 
					        response = self.post(
 | 
				
			||||||
 | 
					            self.URL,
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                'items': [],
 | 
				
			||||||
 | 
					                'customer': 4,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            expected_code=400,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertIn('A list of stock items must be provided', str(response.data))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        stock_item = StockItem.objects.create(
 | 
				
			||||||
 | 
					            part=part.models.Part.objects.get(pk=1),
 | 
				
			||||||
 | 
					            status=StockStatus.DESTROYED,
 | 
				
			||||||
 | 
					            quantity=5,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = self.post(
 | 
				
			||||||
 | 
					            self.URL,
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                'items': [
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        'item': stock_item.pk,
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                'customer': 4,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            expected_code=400,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertIn('Item must be in stock', str(response.data['items'][0]))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_valid(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        stock_items = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for i in range(5):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            stock_item = StockItem.objects.create(
 | 
				
			||||||
 | 
					                part=part.models.Part.objects.get(pk=25),
 | 
				
			||||||
 | 
					                quantity=i + 5,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            stock_items.append({
 | 
				
			||||||
 | 
					                'item': stock_item.pk
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        customer = company.models.Company.objects.get(pk=4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertEqual(customer.assigned_stock.count(), 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = self.post(
 | 
				
			||||||
 | 
					            self.URL,
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                'items': stock_items,
 | 
				
			||||||
 | 
					                'customer': 4,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            expected_code=201,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertEqual(response.data['customer'], 4)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # 5 stock items should now have been assigned to this customer
 | 
				
			||||||
 | 
					        self.assertEqual(customer.assigned_stock.count(), 5)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,7 +23,6 @@ stock_item_detail_urls = [
 | 
				
			|||||||
    url(r'^delete/', views.StockItemDelete.as_view(), name='stock-item-delete'),
 | 
					    url(r'^delete/', views.StockItemDelete.as_view(), name='stock-item-delete'),
 | 
				
			||||||
    url(r'^qr_code/', views.StockItemQRCode.as_view(), name='stock-item-qr'),
 | 
					    url(r'^qr_code/', views.StockItemQRCode.as_view(), name='stock-item-qr'),
 | 
				
			||||||
    url(r'^delete_test_data/', views.StockItemDeleteTestData.as_view(), name='stock-item-delete-test-data'),
 | 
					    url(r'^delete_test_data/', views.StockItemDeleteTestData.as_view(), name='stock-item-delete-test-data'),
 | 
				
			||||||
    url(r'^assign/', views.StockItemAssignToCustomer.as_view(), name='stock-item-assign'),
 | 
					 | 
				
			||||||
    url(r'^return/', views.StockItemReturnToStock.as_view(), name='stock-item-return'),
 | 
					    url(r'^return/', views.StockItemReturnToStock.as_view(), name='stock-item-return'),
 | 
				
			||||||
    url(r'^install/', views.StockItemInstall.as_view(), name='stock-item-install'),
 | 
					    url(r'^install/', views.StockItemInstall.as_view(), name='stock-item-install'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -294,39 +294,6 @@ class StockLocationQRCode(QRCodeView):
 | 
				
			|||||||
            return None
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StockItemAssignToCustomer(AjaxUpdateView):
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    View for manually assigning a StockItem to a Customer
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    model = StockItem
 | 
					 | 
				
			||||||
    ajax_form_title = _("Assign to Customer")
 | 
					 | 
				
			||||||
    context_object_name = "item"
 | 
					 | 
				
			||||||
    form_class = StockForms.AssignStockItemToCustomerForm
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def validate(self, item, form, **kwargs):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        customer = form.cleaned_data.get('customer', None)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if not customer:
 | 
					 | 
				
			||||||
            form.add_error('customer', _('Customer must be specified'))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def save(self, item, form, **kwargs):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Assign the stock item to the customer.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        customer = form.cleaned_data.get('customer', None)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if customer:
 | 
					 | 
				
			||||||
            item = item.allocateToCustomer(
 | 
					 | 
				
			||||||
                customer,
 | 
					 | 
				
			||||||
                user=self.request.user
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            item.clearAllocations()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class StockItemReturnToStock(AjaxUpdateView):
 | 
					class StockItemReturnToStock(AjaxUpdateView):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    View for returning a stock item (which is assigned to a customer) to stock.
 | 
					    View for returning a stock item (which is assigned to a customer) to stock.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,6 +38,7 @@
 | 
				
			|||||||
*/
 | 
					*/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* exported
 | 
					/* exported
 | 
				
			||||||
 | 
					    assignStockToCustomer,
 | 
				
			||||||
    createNewStockItem,
 | 
					    createNewStockItem,
 | 
				
			||||||
    createStockLocation,
 | 
					    createStockLocation,
 | 
				
			||||||
    duplicateStockItem,
 | 
					    duplicateStockItem,
 | 
				
			||||||
@@ -533,13 +534,166 @@ function exportStock(params={}) {
 | 
				
			|||||||
                url += `&${key}=${params[key]}`;
 | 
					                url += `&${key}=${params[key]}`;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            console.log(url);
 | 
					 | 
				
			||||||
            location.href = url;
 | 
					            location.href = url;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Assign multiple stock items to a customer
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function assignStockToCustomer(items, options={}) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Generate HTML content for the form
 | 
				
			||||||
 | 
					    var html = `
 | 
				
			||||||
 | 
					    <table class='table table-striped table-condensed' id='stock-assign-table'>
 | 
				
			||||||
 | 
					    <thead>
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					            <th>{% trans "Part" %}</th>
 | 
				
			||||||
 | 
					            <th>{% trans "Stock Item" %}</th>
 | 
				
			||||||
 | 
					            <th>{% trans "Location" %}</th>
 | 
				
			||||||
 | 
					            <th></th>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					    </thead>
 | 
				
			||||||
 | 
					    <tbody>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (var idx = 0; idx < items.length; idx++) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var item = items[idx];
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        var pk = item.pk;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var part = item.part_detail;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var thumbnail = thumbnailImage(part.thumbnail || part.image);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var status = stockStatusDisplay(item.status, {classes: 'float-right'});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var quantity = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (item.serial && item.quantity == 1) {
 | 
				
			||||||
 | 
					            quantity = `{% trans "Serial" %}: ${item.serial}`;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            quantity = `{% trans "Quantity" %}: ${item.quantity}`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        quantity += status;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var location = locationDetail(item, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var buttons = `<div class='btn-group' role='group'>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        buttons += makeIconButton(
 | 
				
			||||||
 | 
					            'fa-times icon-red',
 | 
				
			||||||
 | 
					            'button-stock-item-remove',
 | 
				
			||||||
 | 
					            pk,
 | 
				
			||||||
 | 
					            '{% trans "Remove row" %}',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        buttons += '</div>';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        html += `
 | 
				
			||||||
 | 
					            <tr id='stock_item_${pk}' class='stock-item'row'>
 | 
				
			||||||
 | 
					                <td id='part_${pk}'>${thumbnail} ${part.full_name}</td>
 | 
				
			||||||
 | 
					                <td id='stock_${pk}'>
 | 
				
			||||||
 | 
					                    <div id='div_id_items_item_${pk}'>
 | 
				
			||||||
 | 
					                        ${quantity}
 | 
				
			||||||
 | 
					                        <div id='errors-items_item_${pk}'></div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </td>
 | 
				
			||||||
 | 
					                <td id='location_${pk}'>${location}</td>
 | 
				
			||||||
 | 
					                <td id='buttons_${pk}'>${buttons}</td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					        `;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    html += `</tbody></table>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructForm('{% url "api-stock-assign" %}', {
 | 
				
			||||||
 | 
					        method: 'POST',
 | 
				
			||||||
 | 
					        preFormContent: html,
 | 
				
			||||||
 | 
					        fields: {
 | 
				
			||||||
 | 
					            'customer': {
 | 
				
			||||||
 | 
					                value: options.customer,
 | 
				
			||||||
 | 
					                filters: {
 | 
				
			||||||
 | 
					                    is_customer: true,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            'notes': {},
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        confirm: true,
 | 
				
			||||||
 | 
					        confirmMessage: '{% trans "Confirm stock assignment" %}',
 | 
				
			||||||
 | 
					        title: '{% trans "Assign Stock to Customer" %}',
 | 
				
			||||||
 | 
					        afterRender: function(fields, opts) {
 | 
				
			||||||
 | 
					            // Add button callbacks to remove rows
 | 
				
			||||||
 | 
					            $(opts.modal).find('.button-stock-item-remove').click(function() {
 | 
				
			||||||
 | 
					                var pk = $(this).attr('pk');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                $(opts.modal).find(`#stock_item_${pk}`).remove();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        onSubmit: function(fields, opts) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Extract data elements from the form
 | 
				
			||||||
 | 
					            var data = {
 | 
				
			||||||
 | 
					                customer: getFormFieldValue('customer', {}, opts),
 | 
				
			||||||
 | 
					                notes: getFormFieldValue('notes', {}, opts),
 | 
				
			||||||
 | 
					                items: [],
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            var item_pk_values = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            items.forEach(function(item) {
 | 
				
			||||||
 | 
					                var pk = item.pk;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Does the row exist in the form?
 | 
				
			||||||
 | 
					                var row = $(opts.modal).find(`#stock_item_${pk}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (row.exists()) {
 | 
				
			||||||
 | 
					                    item_pk_values.push(pk);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    data.items.push({
 | 
				
			||||||
 | 
					                        item: pk,
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            opts.nested = {
 | 
				
			||||||
 | 
					                'items': item_pk_values,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            inventreePut(
 | 
				
			||||||
 | 
					                '{% url "api-stock-assign" %}',
 | 
				
			||||||
 | 
					                data,
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    method: 'POST',
 | 
				
			||||||
 | 
					                    success: function(response) {
 | 
				
			||||||
 | 
					                        $(opts.modal).modal('hide');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (options.success) {
 | 
				
			||||||
 | 
					                            options.success(response);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    error: function(xhr) {
 | 
				
			||||||
 | 
					                        switch (xhr.status) {
 | 
				
			||||||
 | 
					                        case 400:
 | 
				
			||||||
 | 
					                            handleFormErrors(xhr.responseJSON, fields, opts);
 | 
				
			||||||
 | 
					                            break;
 | 
				
			||||||
 | 
					                        default:
 | 
				
			||||||
 | 
					                            $(opts.modal).modal('hide');
 | 
				
			||||||
 | 
					                            showApiError(xhr, opts.url);
 | 
				
			||||||
 | 
					                            break;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Perform stock adjustments
 | 
					 * Perform stock adjustments
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
@@ -777,7 +931,7 @@ function adjustStock(action, items, options={}) {
 | 
				
			|||||||
                // Does the row exist in the form?
 | 
					                // Does the row exist in the form?
 | 
				
			||||||
                var row = $(opts.modal).find(`#stock_item_${pk}`);
 | 
					                var row = $(opts.modal).find(`#stock_item_${pk}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (row) {
 | 
					                if (row.exists()) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    item_pk_values.push(pk);
 | 
					                    item_pk_values.push(pk);
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
@@ -1098,7 +1252,7 @@ function locationDetail(row, showLink=true) {
 | 
				
			|||||||
        // StockItem has been assigned to a sales order
 | 
					        // StockItem has been assigned to a sales order
 | 
				
			||||||
        text = '{% trans "Assigned to Sales Order" %}';
 | 
					        text = '{% trans "Assigned to Sales Order" %}';
 | 
				
			||||||
        url = `/order/sales-order/${row.sales_order}/`;
 | 
					        url = `/order/sales-order/${row.sales_order}/`;
 | 
				
			||||||
    } else if (row.location) {
 | 
					    } else if (row.location && row.location_detail) {
 | 
				
			||||||
        text = row.location_detail.pathstring;
 | 
					        text = row.location_detail.pathstring;
 | 
				
			||||||
        url = `/stock/location/${row.location}/`;
 | 
					        url = `/stock/location/${row.location}/`;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
@@ -1721,6 +1875,17 @@ function loadStockTable(table, options) {
 | 
				
			|||||||
        stockAdjustment('move');
 | 
					        stockAdjustment('move');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $('#multi-item-assign').click(function() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var items = $(table).bootstrapTable('getSelections');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assignStockToCustomer(items, {
 | 
				
			||||||
 | 
					            success: function() {
 | 
				
			||||||
 | 
					                $(table).bootstrapTable('refresh');
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $('#multi-item-order').click(function() {
 | 
					    $('#multi-item-order').click(function() {
 | 
				
			||||||
        var selections = $(table).bootstrapTable('getSelections');
 | 
					        var selections = $(table).bootstrapTable('getSelections');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -50,6 +50,7 @@
 | 
				
			|||||||
                    <li><a class='dropdown-item' href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'><span class='fas fa-check-circle'></span> {% trans "Count stock" %}</a></li>
 | 
					                    <li><a class='dropdown-item' href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'><span class='fas fa-check-circle'></span> {% trans "Count stock" %}</a></li>
 | 
				
			||||||
                    <li><a class='dropdown-item' href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'><span class='fas fa-exchange-alt'></span> {% trans "Move stock" %}</a></li>
 | 
					                    <li><a class='dropdown-item' href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'><span class='fas fa-exchange-alt'></span> {% trans "Move stock" %}</a></li>
 | 
				
			||||||
                    <li><a class='dropdown-item' href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li>
 | 
					                    <li><a class='dropdown-item' href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li>
 | 
				
			||||||
 | 
					                    <li><a class='dropdown-item' href='#' id='multi-item-assign' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
 | 
				
			||||||
                    <li><a class='dropdown-item' href='#' id='multi-item-set-status' title='{% trans "Change status" %}'><span class='fas fa-exclamation-circle'></span> {% trans "Change stock status" %}</a></li>
 | 
					                    <li><a class='dropdown-item' href='#' id='multi-item-set-status' title='{% trans "Change status" %}'><span class='fas fa-exclamation-circle'></span> {% trans "Change stock status" %}</a></li>
 | 
				
			||||||
                    {% endif %}
 | 
					                    {% endif %}
 | 
				
			||||||
                    {% if roles.stock.delete %}
 | 
					                    {% if roles.stock.delete %}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user