Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/backend/InvenTree/InvenTree/api_version.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""InvenTree API version information."""

# InvenTree API version
INVENTREE_API_VERSION = 460
INVENTREE_API_VERSION = 461
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""

INVENTREE_API_TEXT = """

v461 -> 2026-03-10 : https://github.com/inventree/InvenTree/pull/11479
- Adds option to copy parameters when duplicating an order via the API

v460 -> 2026-02-25 : https://github.com/inventree/InvenTree/pull/11374
- Adds "updated_at" field to PurchaseOrder, SalesOrder and ReturnOrder API endpoints
- Adds "updated_before" and "updated_after" date filters to all three order list endpoints
Expand Down
13 changes: 12 additions & 1 deletion src/backend/InvenTree/order/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ class DuplicateOrderSerializer(serializers.Serializer):
class Meta:
"""Metaclass options."""

fields = ['order_id', 'copy_lines', 'copy_extra_lines']
fields = ['order_id', 'copy_lines', 'copy_extra_lines', 'copy_parameters']

order_id = serializers.IntegerField(
required=True, label=_('Order ID'), help_text=_('ID of the order to duplicate')
Expand All @@ -95,6 +95,13 @@ class Meta:
help_text=_('Copy extra line items from the original order'),
)

copy_parameters = serializers.BooleanField(
required=False,
default=True,
label=_('Copy Parameters'),
help_text=_('Copy order parameters from the original order'),
)


class AbstractOrderSerializer(
DataImportExportSerializerMixin, FilterableSerializerMixin, serializers.Serializer
Expand Down Expand Up @@ -242,6 +249,7 @@ def create(self, validated_data):
order_id = duplicate.get('order_id', None)
copy_lines = duplicate.get('copy_lines', True)
copy_extra_lines = duplicate.get('copy_extra_lines', True)
copy_parameters = duplicate.get('copy_parameters', True)

try:
copy_from = instance.__class__.objects.get(pk=order_id)
Expand All @@ -260,6 +268,9 @@ def create(self, validated_data):
line.order = instance
line.save()

if copy_parameters:
instance.copy_parameters_from(copy_from)

return instance


Expand Down
59 changes: 59 additions & 0 deletions src/backend/InvenTree/order/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1574,6 +1574,65 @@ def test_so_create(self):
expected_code=201,
)

def test_so_duplicate(self):
"""Test SalesOrder duplication via the API."""
from common.models import Parameter, ParameterTemplate

url = reverse('api-so-list')

self.assignRole('sales_order.add')

so = models.SalesOrder.objects.get(pk=1)
self.assertEqual(so.status, SalesOrderStatus.PENDING)

# Add some parameters to the sales order
for idx in range(5):
template = ParameterTemplate.objects.create(name=f'Template {idx}')

Parameter.objects.create(
template=template,
model_type=so.get_content_type(),
model_id=so.pk,
data=f'Value {idx}',
)

self.assertEqual(so.parameters.count(), 5)

# Create a duplicate of this sales order
# We explicitly specify "copy_parameters" as False, so the duplicated sales order should not have any parameters
response = self.post(
url,
{
'reference': 'SO-12345',
'customer': so.customer.pk,
'duplicate': {'order_id': so.pk, 'copy_parameters': False},
},
)

duplicate_id = response.data['pk']
duplicate_so = models.SalesOrder.objects.get(pk=duplicate_id)

self.assertEqual(duplicate_so.reference, 'SO-12345')
self.assertEqual(duplicate_so.customer, so.customer)
self.assertEqual(duplicate_so.parameters.count(), 0)

# Duplicate again, with default values for the "duplicate" options (which should result in parameters being copied)
response = self.post(
url,
{
'reference': 'SO-12346',
'customer': so.customer.pk,
'duplicate': {'order_id': so.pk},
},
)

duplicate_id = response.data['pk']
duplicate_so = models.SalesOrder.objects.get(pk=duplicate_id)

self.assertEqual(duplicate_so.reference, 'SO-12346')
self.assertEqual(duplicate_so.customer, so.customer)
self.assertEqual(duplicate_so.parameters.count(), 5)

def test_so_cancel(self):
"""Test API endpoint for cancelling a SalesOrder."""
so = models.SalesOrder.objects.get(pk=1)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Sample supplier plugin."""

from django.conf import settings

from company.models import Company, ManufacturerPart, SupplierPart, SupplierPriceBreak
from part.models import Part
from plugin.mixins import SupplierMixin, supplier
Expand All @@ -13,7 +15,16 @@ class SampleSupplierPlugin(SupplierMixin, InvenTreePlugin):
SLUG = 'samplesupplier'
TITLE = 'My sample supplier plugin'

VERSION = '0.0.1'
VERSION = '0.0.2'

SETTINGS = {
'DOWNLOAD_IMAGES': {
'name': 'Download part images',
'description': 'Enable downloading of part images during import (not recommended during testing)',
'validator': bool,
'default': False,
}
}

def __init__(self):
"""Initialize the sample supplier plugin."""
Expand Down Expand Up @@ -108,7 +119,12 @@ def import_part(self, data, **kwargs) -> Part:

# If the part was created, set additional fields
if created:
if data['image_url']:
# Prevent downloading images during testing, as this can lead to unreliable tests
if (
data['image_url']
and not settings.TESTING
and self.get_setting('DOWNLOAD_IMAGES')
):
file, fmt = self.download_image(data['image_url'])
filename = f'part_{part.pk}_image.{fmt.lower()}'
part.image.save(filename, file)
Expand Down
6 changes: 3 additions & 3 deletions src/backend/requirements-3.14.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1669,9 +1669,9 @@ pynacl==1.6.2 \
# via
# -c src/backend/requirements.txt
# paramiko
pypdf==6.8.0 \
--hash=sha256:2a025080a8dd73f48123c89c57174a5ff3806c71763ee4e49572dc90454943c7 \
--hash=sha256:cb7eaeaa4133ce76f762184069a854e03f4d9a08568f0e0623f7ea810407833b
pypdf==6.7.5 \
--hash=sha256:07ba7f1d6e6d9aa2a17f5452e320a84718d4ce863367f7ede2fd72280349ab13 \
--hash=sha256:40bb2e2e872078655f12b9b89e2f900888bb505e88a82150b64f9f34fa25651d
# via
# -c src/backend/requirements.txt
# -r src/backend/requirements.in
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/src/forms/PurchaseOrderForms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,8 @@ export function usePurchaseOrderFields({
value: duplicateOrderId
},
copy_lines: {},
copy_extra_lines: {}
copy_extra_lines: {},
copy_parameters: {}
}
};
}
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/src/forms/ReturnOrderForms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ export function useReturnOrderFields({
value: false,
hidden: true
},
copy_extra_lines: {}
copy_extra_lines: {},
copy_parameters: {}
}
};
}
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/src/forms/SalesOrderForms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ export function useSalesOrderFields({
value: duplicateOrderId
},
copy_lines: {},
copy_extra_lines: {}
copy_extra_lines: {},
copy_parameters: {}
}
};
}
Expand Down
Loading