Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FR] Support non-serializable (part) item Return Orders #8400

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
12 changes: 6 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ exclude: |
)$
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: mixed-line-ending
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.0
rev: v0.7.1
hooks:
- id: ruff-format
args: [--preview]
Expand All @@ -28,7 +28,7 @@ repos:
--preview
]
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.4.24
rev: 0.4.28
hooks:
- id: pip-compile
name: pip-compile requirements-dev.in
Expand All @@ -51,7 +51,7 @@ repos:
args: [contrib/container/requirements.in, -o, contrib/container/requirements.txt, --python-version=3.11, --no-strip-extras, --generate-hashes]
files: contrib/container/requirements\.(in|txt)$
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.35.2
rev: v1.35.3
hooks:
- id: djlint-django
- repo: https://github.com/codespell-project/codespell
Expand All @@ -78,7 +78,7 @@ repos:
- "prettier@^2.4.1"
- "@trivago/prettier-plugin-sort-imports"
- repo: https://github.com/pre-commit/mirrors-eslint
rev: "v9.12.0"
rev: "v9.13.0"
hooks:
- id: eslint
additional_dependencies:
Expand All @@ -90,7 +90,7 @@ repos:
- "@typescript-eslint/parser"
files: ^src/frontend/.*\.(js|jsx|ts|tsx)$
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.0
rev: v8.21.2
hooks:
- id: gitleaks
#- repo: https://github.com/jumanjihouse/pre-commit-hooks
Expand Down
21 changes: 21 additions & 0 deletions src/backend/InvenTree/order/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,18 @@ class Meta:
clean_model_instances = True


class ReturnOrderPartLineItemResource(PriceResourceMixin, InvenTreeResource):
"""Class for managing import / export of ReturnOrderPartLineItem data."""

class Meta:
"""Metaclass options."""

model = models.ReturnOrderPartLineItem
skip_unchanged = True
report_skipped = False
clean_model_instances = True


class ReturnOrderLineItemAdmin(ImportExportModelAdmin):
"""Admin class for ReturnOrderLine model."""

Expand All @@ -340,6 +352,14 @@ class ReturnOrderLineItemAdmin(ImportExportModelAdmin):
list_display = ['order', 'item', 'reference']


class ReturnOrderPartLineItemAdmin(ImportExportModelAdmin):
"""Admin class for ReturnOrderPartLine model."""

resource_class = ReturnOrderPartLineItemResource

list_display = ['order', 'part', 'reference']


class ReturnOrderExtraLineClass(PriceResourceMixin, InvenTreeResource):
"""Class for managing import/export of ReturnOrderExtraLine data."""

Expand Down Expand Up @@ -370,4 +390,5 @@ class ReturnOrdeerExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin):
# Return Order models
admin.site.register(models.ReturnOrder, ReturnOrderAdmin)
admin.site.register(models.ReturnOrderLineItem, ReturnOrderLineItemAdmin)
admin.site.register(models.ReturnOrderPartLineItem, ReturnOrderPartLineItemAdmin)
admin.site.register(models.ReturnOrderExtraLine, ReturnOrdeerExtraLineAdmin)
116 changes: 116 additions & 0 deletions src/backend/InvenTree/order/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,13 @@ class ReturnOrderReceive(ReturnOrderContextMixin, CreateAPI):
serializer_class = serializers.ReturnOrderReceiveSerializer


class ReturnOrderReceiveParts(ReturnOrderContextMixin, CreateAPI):
"""API endpoint to receive parts against a ReturnOrder."""

queryset = models.ReturnOrder.objects.none()
serializer_class = serializers.ReturnOrderReceivePartsSerializer


class ReturnOrderLineItemFilter(LineItemFilter):
"""Custom filters for the ReturnOrderLineItemList endpoint."""

Expand All @@ -1377,6 +1384,27 @@ def filter_received(self, queryset, name, value):
return queryset.filter(received_date=None)


class ReturnOrderPartLineItemFilter(LineItemFilter):
"""Custom filters for the ReturnOrderPartLineItemList endpoint."""

class Meta:
"""Metaclass options."""

price_field = 'price'
model = models.ReturnOrderPartLineItem
fields = ['order', 'part']

outcome = rest_filters.NumberFilter(label='outcome')

received = rest_filters.BooleanFilter(label='received', method='filter_received')

def filter_received(self, queryset, name, value):
"""Filter by 'received' field."""
if str2bool(value):
return queryset.exclude(received_date=None)
return queryset.filter(received_date=None)


class ReturnOrderLineItemMixin:
"""Mixin class for ReturnOrderLineItem endpoints."""

Expand Down Expand Up @@ -1407,6 +1435,35 @@ def get_queryset(self, *args, **kwargs):
return queryset


class ReturnOrderPartLineItemMixin:
"""Mixin class for ReturnOrderPartLineItem endpoints."""

queryset = models.ReturnOrderPartLineItem.objects.all()
serializer_class = serializers.ReturnOrderPartLineItemSerializer

def get_serializer(self, *args, **kwargs):
"""Return serializer for this endpoint with extra data as requested."""
try:
params = self.request.query_params

kwargs['order_detail'] = str2bool(params.get('order_detail', False))
kwargs['part_detail'] = str2bool(params.get('part_detail', False))
except AttributeError:
pass

kwargs['context'] = self.get_serializer_context()

return self.serializer_class(*args, **kwargs)

def get_queryset(self, *args, **kwargs):
"""Return annotated queryset for this endpoint."""
queryset = super().get_queryset(*args, **kwargs)

queryset = queryset.prefetch_related('order', 'part')

return queryset


class ReturnOrderLineItemList(
ReturnOrderLineItemMixin, DataExportViewMixin, ListCreateAPI
):
Expand All @@ -1426,10 +1483,30 @@ class ReturnOrderLineItemList(
]


class ReturnOrderPartLineItemList(
ReturnOrderPartLineItemMixin, DataExportViewMixin, ListCreateAPI
):
"""API endpoint for accessing a list of ReturnOrderPartLineItemList objects."""

filterset_class = ReturnOrderPartLineItemFilter

filter_backends = SEARCH_ORDER_FILTER

ordering_fields = ['reference', 'target_date', 'received_date']

search_fields = ['part__name', 'part__description', 'reference']


class ReturnOrderLineItemDetail(ReturnOrderLineItemMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a ReturnOrderLineItem object."""


class ReturnOrderPartLineItemDetail(
ReturnOrderPartLineItemMixin, RetrieveUpdateDestroyAPI
):
"""API endpoint for detail view of a ReturnOrderLineItem object."""


class ReturnOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
"""API endpoint for accessing a list of ReturnOrderExtraLine objects."""

Expand Down Expand Up @@ -1863,6 +1940,11 @@ def item_link(self, item):
ReturnOrderReceive.as_view(),
name='api-return-order-receive',
),
path(
'receive-parts/',
ReturnOrderReceiveParts.as_view(),
name='api-return-order-receive-parts',
),
path(
'metadata/',
MetadataView.as_view(),
Expand Down Expand Up @@ -1917,6 +1999,40 @@ def item_link(self, item):
),
]),
),
# API endpoints for return order part lines
path(
'ro-part-line/',
include([
path(
'<int:pk>/',
include([
path(
'metadata/',
MetadataView.as_view(),
{'model': models.ReturnOrderPartLineItem},
name='api-return-order-part-line-metadata',
),
path(
'',
ReturnOrderPartLineItemDetail.as_view(),
name='api-return-order-part-line-detail',
),
]),
),
# Return order part line item status code information
path(
'status/',
StatusView.as_view(),
{StatusView.MODEL_REF: ReturnOrderLineStatus},
name='api-return-order-part-line-status-codes',
),
path(
'',
ReturnOrderPartLineItemList.as_view(),
name='api-return-order-part-line-list',
),
]),
),
# API endpoints for return order extra line
path(
'ro-extra-line/',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 4.2.16 on 2024-10-31 01:20

import InvenTree.fields
import InvenTree.models
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import djmoney.models.fields
import djmoney.models.validators
import generic.states.fields


class Migration(migrations.Migration):

dependencies = [
('part', '0130_alter_parttesttemplate_part'),
('order', '0101_purchaseorder_status_custom_key_and_more'),
]

operations = [
migrations.CreateModel(
name='ReturnOrderPartLineItem',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('metadata', models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata')),
('reference', models.CharField(blank=True, help_text='Line item reference', max_length=100, verbose_name='Reference')),
('notes', models.CharField(blank=True, help_text='Line item notes', max_length=500, verbose_name='Notes')),
('link', InvenTree.fields.InvenTreeURLField(blank=True, help_text='Link to external page', verbose_name='Link')),
('target_date', models.DateField(blank=True, help_text='Target date for this line item (leave blank to use the target date from the order)', null=True, verbose_name='Target Date')),
('quantity', InvenTree.fields.RoundingDecimalField(decimal_places=5, default=0, help_text='Enter stock return quantity', max_digits=15, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Quantity')),
('received_date', models.DateField(blank=True, help_text='The date this this return item was received', null=True, verbose_name='Received Date')),
('outcome', generic.states.fields.InvenTreeCustomStatusModelField(choices=[(10, 'Pending'), (20, 'Return'), (30, 'Repair'), (40, 'Replace'), (50, 'Refund'), (60, 'Reject')], default=10, help_text='Outcome for this line item', verbose_name='Outcome')),
('price_currency', djmoney.models.fields.CurrencyField(choices=[], default='', editable=False, max_length=3, null=True)),
('price', InvenTree.fields.InvenTreeModelMoneyField(blank=True, currency_choices=[], decimal_places=6, default_currency='', help_text='Cost associated with return or repair for this line item', max_digits=19, null=True, validators=[djmoney.models.validators.MinMoneyValidator(0)], verbose_name='Price')),
('outcome_custom_key', generic.states.fields.ExtraInvenTreeCustomStatusModelField(blank=True, default=None, help_text='Additional status information for this item', null=True, verbose_name='Custom status key')),
('order', models.ForeignKey(help_text='Return Order', on_delete=django.db.models.deletion.CASCADE, related_name='part_lines', to='order.returnorder', verbose_name='Order')),
('part', models.ForeignKey(help_text='Select part to return from customer', limit_choices_to={'salable': True, 'virtual': False}, on_delete=django.db.models.deletion.CASCADE, related_name='return_order_part_line_items', to='part.part', verbose_name='Part')),
],
options={
'verbose_name': 'Return Order Part Line Item',
'unique_together': {('order', 'part')},
},
bases=(InvenTree.models.PluginValidationMixin, models.Model),
),
]
Loading