2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-30 18:50:53 +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

@ -90,7 +90,7 @@ function editBuildOrder(pk) {
var fields = buildFormFields();
constructForm(`/api/build/${pk}/`, {
constructForm(`{% url "api-build-list" %}${pk}/`, {
fields: fields,
reload: true,
title: '{% trans "Edit Build Order" %}',
@ -147,7 +147,7 @@ function newBuildOrder(options={}) {
*/
function duplicateBuildOrder(build_id, options={}) {
inventreeGet(`/api/build/${build_id}/`, {}, {
inventreeGet(`{% url "api-build-list" %}${build_id}/`, {}, {
success: function(data) {
// Clear out data we do not want to be duplicated
delete data['pk'];
@ -166,7 +166,7 @@ function duplicateBuildOrder(build_id, options={}) {
function cancelBuildOrder(build_id, options={}) {
constructForm(
`/api/build/${build_id}/cancel/`,
`{% url "api-build-list" %}${build_id}/cancel/`,
{
method: 'POST',
title: '{% trans "Cancel Build Order" %}',
@ -208,7 +208,7 @@ function cancelBuildOrder(build_id, options={}) {
/* Construct a form to "complete" (finish) a build order */
function completeBuildOrder(build_id, options={}) {
constructForm(`/api/build/${build_id}/finish/`, {
constructForm(`{% url "api-build-list" %}${build_id}/finish/`, {
fieldsFunction: function(opts) {
var ctx = opts.context || {};
@ -287,7 +287,7 @@ function createBuildOutput(build_id, options) {
// Request build order information from the server
inventreeGet(
`/api/build/${build_id}/`,
`{% url "api-build-list" %}${build_id}/`,
{},
{
success: function(build) {
@ -312,7 +312,7 @@ function createBuildOutput(build_id, options) {
};
// Work out the next available serial numbers
inventreeGet(`/api/part/${build.part}/serial-numbers/`, {}, {
inventreeGet(`{% url "api-part-list" %}${build.part}/serial-numbers/`, {}, {
success: function(data) {
if (data.next) {
fields.serial_numbers.placeholder = `{% trans "Next available serial number" %}: ${data.next}`;
@ -341,7 +341,7 @@ function createBuildOutput(build_id, options) {
`;
}
constructForm(`/api/build/${build_id}/create-output/`, {
constructForm(`{% url "api-build-list" %}${build_id}/create-output/`, {
method: 'POST',
title: '{% trans "Create Build Output" %}',
confirm: true,
@ -364,7 +364,7 @@ function createBuildOutput(build_id, options) {
*/
function makeBuildOutputButtons(output_id, build_info, options={}) {
var html = `<div class='btn-group float-right' role='group'>`;
var html = '';
// Tracked parts? Must be individually allocated
if (options.has_bom_items) {
@ -398,17 +398,13 @@ function makeBuildOutputButtons(output_id, build_info, options={}) {
);
// Add a button to "delete" this build output
html += makeIconButton(
'fa-trash-alt icon-red',
html += makeDeleteButton(
'button-output-delete',
output_id,
'{% trans "Delete build output" %}',
);
html += `</div>`;
return html;
return wrapButtons(html);
}
@ -421,7 +417,7 @@ function makeBuildOutputButtons(output_id, build_info, options={}) {
*/
function unallocateStock(build_id, options={}) {
var url = `/api/build/${build_id}/unallocate/`;
var url = `{% url "api-build-list" %}${build_id}/unallocate/`;
var html = `
<div class='alert alert-block alert-warning'>
@ -486,7 +482,7 @@ function completeBuildOutputs(build_id, outputs, options={}) {
var buttons = `<div class='btn-group float-right' role='group'>`;
buttons += makeIconButton('fa-times icon-red', 'button-row-remove', pk, '{% trans "Remove row" %}');
buttons += makeRemoveButton('button-row-remove', pk, '{% trans "Remove row" %}');
buttons += '</div>';
@ -529,7 +525,7 @@ function completeBuildOutputs(build_id, outputs, options={}) {
</tbody>
</table>`;
constructForm(`/api/build/${build_id}/complete/`, {
constructForm(`{% url "api-build-list" %}${build_id}/complete/`, {
method: 'POST',
preFormContent: html,
fields: {
@ -647,7 +643,7 @@ function deleteBuildOutputs(build_id, outputs, options={}) {
var buttons = `<div class='btn-group float-right' role='group'>`;
buttons += makeIconButton('fa-times icon-red', 'button-row-remove', pk, '{% trans "Remove row" %}');
buttons += makeRemoveButton('button-row-remove', pk, '{% trans "Remove row" %}');
buttons += '</div>';
@ -690,7 +686,7 @@ function deleteBuildOutputs(build_id, outputs, options={}) {
</tbody>
</table>`;
constructForm(`/api/build/${build_id}/delete-outputs/`, {
constructForm(`{% url "api-build-list" %}${build_id}/delete-outputs/`, {
method: 'POST',
preFormContent: html,
fields: {},
@ -768,11 +764,7 @@ function loadBuildOrderAllocationTable(table, options={}) {
options.params['location_detail'] = true;
options.params['stock_detail'] = true;
var filters = loadTableFilters('buildorderallocation');
for (var key in options.params) {
filters[key] = options.params[key];
}
var filters = loadTableFilters('buildorderallocation', options.params);
setupFilterList('buildorderallocation', $(table));
@ -893,7 +885,12 @@ function loadBuildOutputTable(build_info, options={}) {
filters[key] = params[key];
}
setupFilterList('builditems', $(table), options.filterTarget || '#filter-list-incompletebuilditems');
setupFilterList('builditems', $(table), options.filterTarget || '#filter-list-incompletebuilditems', {
labels: {
url: '{% url "api-stockitem-label-list" %}',
key: 'item',
}
});
function setupBuildOutputButtonCallbacks() {
@ -1407,19 +1404,6 @@ function loadBuildOutputTable(build_info, options={}) {
);
});
// Print stock item labels
$('#incomplete-output-print-label').click(function() {
var outputs = getTableData(table);
var stock_id_values = [];
outputs.forEach(function(output) {
stock_id_values.push(output.pk);
});
printStockItemLabels(stock_id_values);
});
$('#outputs-expand').click(function() {
$(table).bootstrapTable('expandAllRows');
});
@ -1482,13 +1466,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
}
// Filters
var filters = loadTableFilters('builditems');
var params = options.params || {};
for (var key in params) {
filters[key] = params[key];
}
let filters = loadTableFilters('builditems', options.params);
setupFilterList('builditems', $(table), options.filterTarget);
@ -1703,6 +1681,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
name: 'build-allocation',
uniqueId: 'sub_part',
search: options.search || false,
queryParams: filters,
original: options.params,
onPostBody: function(data) {
// Setup button callbacks
setupCallbacks();
@ -1796,15 +1776,13 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
var pk = row.pk;
var html = `<div class='btn-group float-right' role='group'>`;
var html = '';
html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
html += makeEditButton('button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
html += makeDeleteButton('button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
html += `</div>`;
return html;
return wrapButtons(html);
}
}
]
@ -1814,7 +1792,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
subTable.find('.button-allocation-edit').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/build/item/${pk}/`, {
constructForm(`{% url "api-build-item-list" %}${pk}/`, {
fields: {
quantity: {},
},
@ -1826,7 +1804,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
subTable.find('.button-allocation-delete').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/build/item/${pk}/`, {
constructForm(`{% url "api-build-item-list" %}${pk}/`, {
method: 'DELETE',
title: '{% trans "Remove Allocation" %}',
onSuccess: reloadAllocationData,
@ -1935,9 +1913,9 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
icons += `<span class='fas fa-info-circle icon-blue float-right' title='{% trans "Consumable item" %}'></span>`;
} else {
if (available_stock < (required - allocated)) {
icons += `<span class='fas fa-times-circle icon-red float-right' title='{% trans "Insufficient stock available" %}'></span>`;
icons += makeIconBadge('fa-times-circle icon-red', '{% trans "Insufficient stock available" %}');
} else {
icons += `<span class='fas fa-check-circle icon-green float-right' title='{% trans "Sufficient stock available" %}'></span>`;
icons += makeIconBadge('fa-check-circle icon-green', '{% trans "Sufficient stock available" %}');
}
if (available_stock <= 0) {
@ -1953,13 +1931,15 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
}
if (extra) {
icons += `<span title='${extra}' class='fas fa-info-circle float-right icon-blue'></span>`;
icons += makeInfoButton('fa-info-circle icon-blue', extra);
}
}
}
if (row.on_order && row.on_order > 0) {
icons += `<span class='fas fa-shopping-cart float-right' title='{% trans "On Order" %}: ${row.on_order}'></span>`;
makeIconBadge('fa-shopping-cart', '{% trans "On Order" %}', {
content: row.on_order,
});
}
return renderLink(text, url) + icons;
@ -2027,7 +2007,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
}
// Generate action buttons for this build output
var html = `<div class='btn-group float-right' role='group'>`;
let html = '';
if (allocatedQuantity(row) < requiredQuantity(row)) {
if (row.sub_part_detail.assembly) {
@ -2041,17 +2021,16 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', row.sub_part, '{% trans "Allocate stock" %}');
}
html += makeIconButton(
'fa-minus-circle icon-red', 'button-unallocate', row.sub_part,
html += makeRemoveButton(
'button-unallocate',
row.sub_part,
'{% trans "Unallocate stock" %}',
{
disabled: allocatedQuantity(row) == 0,
}
);
html += '</div>';
return html;
return wrapButtons(html);
}
},
]
@ -2093,7 +2072,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
if (output_id) {
// Request information on the particular build output (stock item)
inventreeGet(`/api/stock/${output_id}/`, {}, {
inventreeGet(`{% url "api-stock-list" %}${output_id}/`, {}, {
success: function(output) {
if (output.quantity == 1 && output.serial != null) {
auto_fill_filters.serial = output.serial;
@ -2112,8 +2091,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
var delete_button = `<div class='btn-group float-right' role='group'>`;
delete_button += makeIconButton(
'fa-times icon-red',
delete_button += makeRemoveButton(
'button-row-remove',
pk,
'{% trans "Remove row" %}',
@ -2245,7 +2223,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
</table>
`;
constructForm(`/api/build/${build_id}/allocate/`, {
constructForm(`{% url "api-build-list" %}${build_id}/allocate/`, {
method: 'POST',
fields: {},
preFormContent: html,
@ -2459,7 +2437,7 @@ function autoAllocateStockToBuild(build_id, bom_items=[], options={}) {
},
};
constructForm(`/api/build/${build_id}/auto-allocate/`, {
constructForm(`{% url "api-build-list" %}${build_id}/auto-allocate/`, {
method: 'POST',
fields: fields,
title: '{% trans "Allocate Stock Items" %}',
@ -2484,21 +2462,19 @@ function loadBuildTable(table, options) {
var params = options.params || {};
var filters = {};
params['part_detail'] = true;
if (!options.disableFilters) {
filters = loadTableFilters('build');
}
for (var key in params) {
filters[key] = params[key];
}
var filters = loadTableFilters('build', params);
var filterTarget = options.filterTarget || null;
setupFilterList('build', table, filterTarget, {download: true});
setupFilterList('build', table, filterTarget, {
download: true,
report: {
url: '{% url "api-build-report-list" %}',
key: 'build',
}
});
// Which display mode to use for the build table?
var display_mode = inventreeLoad('build-table-display-mode', 'list');