2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-01 03:00:54 +00:00

[Feature] Add RMA support (#4488)

* Adds ReturnOrder and ReturnOrderAttachment models

* Adds new 'role' specific for return orders

* Refactor total_price into a mixin

- Required for PurchaseOrder and SalesOrder
- May not be required for ReturnOrder (remains to be seen)

* Adds API endpoints for ReturnOrder

- Add list endpoint
- Add detail endpoint
- Adds required serializer models

* Adds basic "index" page for Return Order model

* Update API version

* Update navbar text

* Add db migration for new "role"

* Add ContactList and ContactDetail API endpoints

* Adds template and JS code for manipulation of contacts

- Display a table
- Create / edit / delete

* Splits order.js into multiple files

- Javascript files was becoming extremely large
- Hard to debug and find code
- Split into purchase_order / return_order / sales_order

* Fix role name (change 'returns' to 'return_order')

- Similar to existing roles for purchase_order and sales_order

* Adds detail page for ReturnOrder

* URL cleanup

- Use <int:pk> instead of complex regex

* More URL cleanup

* Add "return orders" list to company detail page

* Break JS status codes into new javascript file

- Always difficult to track down where these are rendered
- Enough to warrant their own file now

* Add ability to edit return order from detail page

* Database migrations

- Add new ReturnOrder modeles
- Add new 'contact' field to external orders

* Adds "contact" to ReturnOrder

- Implement check to ensure that the selected "contact" matches the selected "company"

* Adjust filters to limit contact options

* Fix typo

* Expose 'contact' field for PurchaseOrder model

* Render contact information

* Add "contact" for SalesOrder

* Adds setting to enable / disable return order functionality

- Simply hides the navigation elements
- API is not disabled

* Support filtering ReturnOrder by 'status'

- Refactors existing filter into the OrderFilter class

* js linting

* More JS linting

* Adds ReturnOrderReport model

* Add serializer for the ReturnOrderReport model

- A little bit of refactoring along the way

* Admin integration for new report model

* Refactoring for report.api

- Adds generic mixins for filtering queryset (based on updates to label.api)
- Reduces repeated code a *lot*

* Exposes API endpoints for ReturnOrderReport

* Adds default example report file for ReturnOrder

- Requires some more work :)

* Refactor report printing javascript code

- Replace all existing functions with 'printReports'

* Improvements for default StockItem test report template

- Fix bug in template
- Handle potential errors in template tags
- Add more helpers to report tags
- Improve test result rendering

* Reduce logging verbosity from weasyprint

* Refactor javascript for label printing

- Consolidate into a single function
- Similar to refactor of report functions

* Add report print button to return order page

* Record user reference when creating via API

* Refactor order serializers

- Move common code into AbstractOrderSerializer class

* Adds extra line item model for the return order

- Adds serializer and API endpoints as appropriate

* Render extra line table for return order

- Refactor existing functions into a single generic function
- Reduces repeated JS code a lot

* Add ability to create a new extra line item

* Adds button for creating a new lien item

* JS linting

* Update test

* Typo fix

(cherry picked from commit 28ac2be35b)

* Enable search for return order

* Don't do pricing (yet) for returnorder extra line table

- Fixes an uncaught error

* Error catching for api.js

* Updates for order models:

- Add 'target_date' field to abstract Order model
- Add IN_PROGRESS status code for return order
- Refactor 'overdue' and 'outstanding' API queries
- Refactor OVERDUE_FILTER on order models
- Refactor is_overdue on order models
- More table filters for return order model

* JS cleanup

* Create ReturnOrderLineItem model

- New type of status label
- Add TotalPriceMixin to ReturnOrder model

* Adds an API serializer for the ReturnOrderLineItem model

* Add API endpoints for ReturnOrderLineItem model

- Including some refactoring along the way

* javascript: refactor loadTableFilters function

- Pass enforced query through to the filters
- Call Object.assign() to construct a superset query
- Removes a lot of code duplication

* Refactor hard-coded URLS to use {% url %} lookup

- Forces error if the URL is wrong
- If we ever change the URL, will still work

* Implement creation of new return order line items

* Adds 'part_detail' annotation to ReturnOrderLineItem serializer

- Required for rendering part information

* javascript: refactor method for creating a group of buttons in a table

* javascript: refactor common buttons with helper functions

* Allow edit and delete of return order line items

* Add form option to automatically reload a table on success

- Pass table name to options.refreshTable

* JS linting

* Add common function for createExtraLineItem

* Refactor loading of attachment tables

- Setup drag-and-drop as part of core function

* CI fixes

* Refactoring out some more common API endpoint code

* Update migrations

* Fix permission typo

* Refactor for unit testing code

* Add unit tests for Contact model

* Tests for returnorder list API

* Annotate 'line_items' to ReturnOrder serializer

* Driving the refactor tractor

* More unit tests for the ReturnOrder API endpoints

* Refactor "print orders" button for various order tables

- Move into "setupFilterList" code (generic)

* add generic 'label printing' button to table actions buttons

* Refactor build output table

* Refactoring icon generation for js

* Refactoring for Part API

* Fix database model type for 'received_date'

* Add API endpoint to "issue" a ReturnOrder

* Improvements for stock tracking table

- Add new status codes
- Add rendering for SalesOrder
- Add rendering for ReturnOrder
- Fix status badges

* Adds functionality to receive line items against a return order

* Add endpoints for completing and cancelling orders

* Add option to allow / prevent editing of ReturnOrder after completed

* js linting

* Wrap "add extra line" button in setting check

* Updates to order/admin.py

* Remove inline admin for returnorderline model

* Updates to pass CI

* Serializer fix

* order template fixes

* Unit test fix

* Fixes for ReturnOrder.receive_line_item

* Unit testing for receiving line items against an RMA

* Improve example report for return order

* Extend unit tests for reporting

* Cleanup here and there

* Unit testing for order views

* Clear "sales_order" field when returning against ReturnOrder

* Add 'location' to deltas when returning from customer

* Bug fix for unit test
This commit is contained in:
Oliver
2023-03-29 10:35:43 +11:00
committed by GitHub
parent d4a64b4f7d
commit 27aa16d55d
122 changed files with 10391 additions and 7053 deletions

View File

@ -30,8 +30,10 @@ from InvenTree.helpers import (DownloadFile, extract_serial_numbers, isNull,
from InvenTree.mixins import (CreateAPI, CustomRetrieveUpdateDestroyAPI,
ListAPI, ListCreateAPI, RetrieveAPI,
RetrieveUpdateAPI, RetrieveUpdateDestroyAPI)
from order.models import PurchaseOrder, SalesOrder, SalesOrderAllocation
from order.serializers import PurchaseOrderSerializer
from order.models import (PurchaseOrder, ReturnOrder, SalesOrder,
SalesOrderAllocation)
from order.serializers import (PurchaseOrderSerializer, ReturnOrderSerializer,
SalesOrderSerializer)
from part.models import BomItem, Part, PartCategory
from part.serializers import PartBriefSerializer
from plugin.serializers import MetadataSerializer
@ -1262,7 +1264,7 @@ class StockTrackingList(ListAPI):
except Exception:
pass
# Add purchaseorder detail
# Add PurchaseOrder detail
if 'purchaseorder' in deltas:
try:
order = PurchaseOrder.objects.get(pk=deltas['purchaseorder'])
@ -1271,6 +1273,24 @@ class StockTrackingList(ListAPI):
except Exception:
pass
# Add SalesOrder detail
if 'salesorder' in deltas:
try:
order = SalesOrder.objects.get(pk=deltas['salesorder'])
serializer = SalesOrderSerializer(order)
deltas['salesorder_detail'] = serializer.data
except Exception:
pass
# Add ReturnOrder detail
if 'returnorder' in deltas:
try:
order = ReturnOrder.objects.get(pk=deltas['returnorder'])
serializer = ReturnOrderSerializer(order)
deltas['returnorder_detail'] = serializer.data
except Exception:
pass
if request.is_ajax():
return JsonResponse(data, safe=False)
else:
@ -1368,7 +1388,7 @@ stock_api_urls = [
re_path(r'^tree/', StockLocationTree.as_view(), name='api-location-tree'),
# Stock location detail endpoints
re_path(r'^(?P<pk>\d+)/', include([
path(r'<int:pk>/', include([
re_path(r'^metadata/', LocationMetadata.as_view(), name='api-location-metadata'),
@ -1388,24 +1408,24 @@ stock_api_urls = [
# StockItemAttachment API endpoints
re_path(r'^attachment/', include([
re_path(r'^(?P<pk>\d+)/', StockAttachmentDetail.as_view(), name='api-stock-attachment-detail'),
path(r'<int:pk>/', StockAttachmentDetail.as_view(), name='api-stock-attachment-detail'),
path('', StockAttachmentList.as_view(), name='api-stock-attachment-list'),
])),
# StockItemTestResult API endpoints
re_path(r'^test/', include([
re_path(r'^(?P<pk>\d+)/', StockItemTestResultDetail.as_view(), name='api-stock-test-result-detail'),
path(r'<int:pk>/', StockItemTestResultDetail.as_view(), name='api-stock-test-result-detail'),
re_path(r'^.*$', StockItemTestResultList.as_view(), name='api-stock-test-result-list'),
])),
# StockItemTracking API endpoints
re_path(r'^track/', include([
re_path(r'^(?P<pk>\d+)/', StockTrackingDetail.as_view(), name='api-stock-tracking-detail'),
path(r'<int:pk>/', StockTrackingDetail.as_view(), name='api-stock-tracking-detail'),
re_path(r'^.*$', StockTrackingList.as_view(), name='api-stock-tracking-list'),
])),
# Detail views for a single stock item
re_path(r'^(?P<pk>\d+)/', include([
path(r'<int:pk>/', include([
re_path(r'^convert/', StockItemConvert.as_view(), name='api-stock-item-convert'),
re_path(r'^install/', StockItemInstall.as_view(), name='api-stock-item-install'),
re_path(r'^metadata/', StockMetadata.as_view(), name='api-stock-item-metadata'),

View File

@ -457,8 +457,6 @@ class StockItem(InvenTreeBarcodeMixin, MetadataMixin, common.models.MetaMixin, M
if old.status != self.status:
deltas['status'] = self.status
# TODO - Other interesting changes we are interested in...
if add_note and len(deltas) > 0:
self.add_tracking_entry(
StockHistoryCode.EDITED,
@ -960,17 +958,22 @@ class StockItem(InvenTreeBarcodeMixin, MetadataMixin, common.models.MetaMixin, M
item.customer = customer
item.location = None
item.save()
item.save(add_note=False)
# TODO - Remove any stock item allocations from this stock item
code = StockHistoryCode.SENT_TO_CUSTOMER
deltas = {
'customer': customer.pk,
'customer_name': customer.pk,
}
if order:
code = StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER
deltas['salesorder'] = order.pk
item.add_tracking_entry(
StockHistoryCode.SENT_TO_CUSTOMER,
code,
user,
{
'customer': customer.id,
'customer_name': customer.name,
},
deltas,
notes=notes,
)
@ -992,7 +995,9 @@ class StockItem(InvenTreeBarcodeMixin, MetadataMixin, common.models.MetaMixin, M
"""
notes = kwargs.get('notes', '')
tracking_info = {}
tracking_info = {
'location': location.pk,
}
if self.customer:
tracking_info['customer'] = self.customer.id

View File

@ -222,30 +222,18 @@
);
});
enableDragAndDrop(
'#attachment-dropzone',
"{% url 'api-stock-attachment-list' %}",
{
data: {
stock_item: {{ item.id }},
},
label: 'attachment',
success: function(data, status, xhr) {
reloadAttachmentTable();
onPanelLoad('attachments', function() {
loadAttachmentTable('{% url "api-stock-attachment-list" %}', {
filters: {
stock_item: {{ item.pk }},
},
fields: {
stock_item: {
value: {{ item.pk }},
hidden: true,
}
}
);
loadAttachmentTable('{% url "api-stock-attachment-list" %}', {
filters: {
stock_item: {{ item.pk }},
},
fields: {
stock_item: {
value: {{ item.pk }},
hidden: true,
}
}
});
});
loadStockTestResultsTable(
@ -255,12 +243,12 @@
}
);
function reloadTable() {
$("#test-result-table").bootstrapTable("refresh");
}
$("#test-report").click(function() {
printTestReports([{{ item.pk }}]);
printReports({
items: [{{ item.pk }}],
key: 'item',
url: '{% url "api-stockitem-testreport-list" %}',
});
});
{% if user.is_staff %}
@ -299,7 +287,7 @@
method: 'DELETE',
title: '{% trans "Delete Test Data" %}',
preFormContent: html,
onSuccess: reloadTable,
refreshTable: '#test-result-table',
});
}
}
@ -315,7 +303,7 @@
stock_item: {{ item.pk }},
}),
title: '{% trans "Add Test Result" %}',
onSuccess: reloadTable,
refreshTable: '#test-result-table',
});
});

View File

@ -493,11 +493,19 @@ $('#stock-uninstall').click(function() {
});
$("#stock-test-report").click(function() {
printTestReports([{{ item.pk }}]);
printReports({
items: [{{ item.pk }}],
key: 'item',
url: '{% url "api-stockitem-testreport-list" %}',
});
});
$("#print-label").click(function() {
printStockItemLabels([{{ item.pk }}]);
printLabels({
items: [{{ item.pk }}],
url: '{% url "api-stockitem-label-list" %}',
key: 'item',
});
});
{% if roles.stock.change %}

View File

@ -228,17 +228,6 @@
<div class='panel-content'>
<div id='sublocation-button-toolbar'>
<div class='btn-group' role='group'>
<!-- Printing actions menu -->
{% if labels_enabled %}
<div class='btn-group' role='group'>
<button id='location-print-options' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle="dropdown" title='{% trans "Printing Actions" %}'>
<span class='fas fa-print'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu'>
<li><a class='dropdown-item' href='#' id='multi-location-print-label' title='{% trans "Print labels" %}'><span class='fas fa-tags'></span> {% trans "Print labels" %}</a></li>
</ul>
</div>
{% endif %}
{% include "filter_list.html" with id="location" %}
</div>
</div>
@ -299,21 +288,11 @@
var locs = [{{ location.pk }}];
printStockLocationLabels(locs);
});
$('#multi-location-print-label').click(function() {
var selections = getTableData('#sublocation-table');
var locations = [];
selections.forEach(function(loc) {
locations.push(loc.pk);
printLabels({
items: locs,
key: 'location',
url: '{% url "api-stocklocation-label-list" %}',
});
printStockLocationLabels(locations);
});
{% endif %}

View File

@ -491,7 +491,7 @@ class StockTest(StockTestBase):
# Check that a tracking item was added
track = StockItemTracking.objects.filter(item=ait).latest('id')
self.assertEqual(track.tracking_type, StockHistoryCode.SENT_TO_CUSTOMER)
self.assertEqual(track.tracking_type, StockHistoryCode.SHIPPED_AGAINST_SALES_ORDER)
self.assertIn('Allocated some stock', track.notes)
def test_return_from_customer(self):

View File

@ -1,12 +1,12 @@
"""URL lookup for Stock app."""
from django.urls import include, re_path
from django.urls import include, path, re_path
from stock import views
location_urls = [
re_path(r'^(?P<pk>\d+)/', include([
path(r'<int:pk>/', include([
# Anything else - direct to the location detail view
re_path('^.*$', views.StockLocationDetail.as_view(), name='stock-location-detail'),
])),