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:
@ -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'),
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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'),
|
||||
])),
|
||||
|
Reference in New Issue
Block a user