2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-07-09 07:00:56 +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

@ -4,23 +4,26 @@
constructForm,
imageHoverIcon,
loadTableFilters,
makeIconButton,
renderLink,
setupFilterList,
*/
/* exported
createCompany,
createContact,
createManufacturerPart,
createSupplierPart,
createSupplierPartPriceBreak,
deleteContacts,
deleteManufacturerParts,
deleteManufacturerPartParameters,
deleteSupplierParts,
duplicateSupplierPart,
editCompany,
editContact,
editSupplierPartPriceBreak,
loadCompanyTable,
loadContactTable,
loadManufacturerPartTable,
loadManufacturerPartParameterTable,
loadSupplierPartTable,
@ -197,7 +200,7 @@ function createSupplierPart(options={}) {
var header = '';
if (options.part) {
var part_model = {};
inventreeGet(`/api/part/${options.part}/.*`, {}, {
inventreeGet(`{% url "api-part-list" %}${options.part}/.*`, {}, {
async: false,
success: function(response) {
part_model = response;
@ -226,7 +229,7 @@ function duplicateSupplierPart(part, options={}) {
var fields = options.fields || supplierPartFields();
// Retrieve information for the supplied part
inventreeGet(`/api/company/part/${part}/`, {}, {
inventreeGet(`{% url "api-supplier-part-list" %}${part}/`, {}, {
success: function(data) {
// Remove fields which we do not want to duplicate
@ -234,7 +237,7 @@ function duplicateSupplierPart(part, options={}) {
delete data['available'];
delete data['availability_updated'];
constructForm(`/api/company/part/`, {
constructForm('{% url "api-supplier-part-list" %}', {
method: 'POST',
fields: fields,
title: '{% trans "Duplicate Supplier Part" %}',
@ -260,7 +263,7 @@ function editSupplierPart(part, options={}) {
fields.part.hidden = true;
}
constructForm(`/api/company/part/${part}/`, {
constructForm(`{% url "api-supplier-part-list" %}${part}/`, {
fields: fields,
title: options.title || '{% trans "Edit Supplier Part" %}',
onSuccess: options.onSuccess
@ -443,24 +446,18 @@ function createCompany(options={}) {
}
/*
* Load company listing data into specified table.
*
* Args:
* - table: Table element on the page
* - url: Base URL for the API query
* - options: table options.
*/
function loadCompanyTable(table, url, options={}) {
/*
* Load company listing data into specified table.
*
* Args:
* - table: Table element on the page
* - url: Base URL for the API query
* - options: table options.
*/
// Query parameters
var params = options.params || {};
var filters = loadTableFilters('company');
for (var key in params) {
filters[key] = params[key];
}
let params = options.params || {};
let filters = loadTableFilters('company', params);
setupFilterList('company', $(table));
@ -547,6 +544,230 @@ function loadCompanyTable(table, url, options={}) {
}
/*
* Construct a set of form fields for the Contact model
*/
function contactFields(options={}) {
let fields = {
company: {
icon: 'fa-building',
},
name: {
icon: 'fa-user',
},
phone: {
icon: 'fa-phone'
},
email: {
icon: 'fa-at',
},
role: {
icon: 'fa-user-tag',
},
};
if (options.company) {
fields.company.value = options.company;
}
return fields;
}
/*
* Launches a form to create a new Contact
*/
function createContact(options={}) {
let fields = options.fields || contactFields(options);
constructForm('{% url "api-contact-list" %}', {
method: 'POST',
fields: fields,
title: '{% trans "Create New Contact" %}',
onSuccess: function(response) {
handleFormSuccess(response, options);
}
});
}
/*
* Launches a form to edit an existing Contact
*/
function editContact(pk, options={}) {
let fields = options.fields || contactFields(options);
constructForm(`{% url "api-contact-list" %}${pk}/`, {
fields: fields,
title: '{% trans "Edit Contact" %}',
onSuccess: function(response) {
handleFormSuccess(response, options);
}
});
}
/*
* Launches a form to delete one (or more) contacts
*/
function deleteContacts(contacts, options={}) {
if (contacts.length == 0) {
return;
}
function renderContact(contact) {
return `
<tr>
<td>${contact.name}</td>
<td>${contact.email}</td>
<td>${contact.role}</td>
</tr>`;
}
let rows = '';
let ids = [];
contacts.forEach(function(contact) {
rows += renderContact(contact);
ids.push(contact.pk);
});
let html = `
<div class='alert alert-block alert-danger'>
{% trans "All selected contacts will be deleted" %}
</div>
<table class='table table-striped table-condensed'>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Role" %}</th>
</tr>
${rows}
</table>`;
constructForm('{% url "api-contact-list" %}', {
method: 'DELETE',
multi_delete: true,
title: '{% trans "Delete Contacts" %}',
preFormContent: html,
form_data: {
items: ids,
},
onSuccess: function(response) {
handleFormSuccess(response, options);
}
});
}
/*
* Load table listing company contacts
*/
function loadContactTable(table, options={}) {
var params = options.params || {};
var filters = loadTableFilters('contact', params);
setupFilterList('contact', $(table), '#filter-list-contacts');
$(table).inventreeTable({
url: '{% url "api-contact-list" %}',
queryParams: filters,
original: params,
idField: 'pk',
uniqueId: 'pk',
sidePagination: 'server',
formatNoMatches: function() {
return '{% trans "No contacts found" %}';
},
showColumns: true,
name: 'contacts',
columns: [
{
field: 'name',
title: '{% trans "Name" %}',
sortable: true,
switchable: false,
},
{
field: 'phone',
title: '{% trans "Phone Number" %}',
sortable: false,
switchable: true,
},
{
field: 'email',
title: '{% trans "Email Address" %}',
sortable: false,
switchable: true,
},
{
field: 'role',
title: '{% trans "Role" %}',
sortable: false,
switchable: false,
},
{
field: 'actions',
title: '',
sortable: false,
switchable: false,
visible: options.allow_edit || options.allow_delete,
formatter: function(value, row) {
var pk = row.pk;
let html = '';
if (options.allow_edit) {
html += makeEditButton('btn-contact-edit', pk, '{% trans "Edit Contact" %}');
}
if (options.allow_delete) {
html += makeDeleteButton('btn-contact-delete', pk, '{% trans "Delete Contact" %}');
}
return wrapButtons(html);
}
}
],
onPostBody: function() {
// Edit button callback
if (options.allow_edit) {
$(table).find('.btn-contact-edit').click(function() {
var pk = $(this).attr('pk');
editContact(pk, {
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
});
}
// Delete button callback
if (options.allow_delete) {
$(table).find('.btn-contact-delete').click(function() {
var pk = $(this).attr('pk');
var row = $(table).bootstrapTable('getRowByUniqueId', pk);
if (row && row.pk) {
deleteContacts([row], {
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
}
});
}
}
});
}
/* Delete one or more ManufacturerPart objects from the database.
* - User will be provided with a modal form, showing all the parts to be deleted.
* - Delete operations are performed sequentialy, not simultaneously
@ -653,21 +874,16 @@ function deleteManufacturerPartParameters(selections, options={}) {
}
/*
* Load manufacturer part table
*/
function loadManufacturerPartTable(table, url, options) {
/*
* Load manufacturer part table
*
*/
// Query parameters
var params = options.params || {};
// Load filters
var filters = loadTableFilters('manufacturer-part');
for (var key in params) {
filters[key] = params[key];
}
var filters = loadTableFilters('manufacturer-part', params);
var filterTarget = options.filterTarget || '#filter-list-manufacturer-part';
@ -703,11 +919,11 @@ function loadManufacturerPartTable(table, url, options) {
var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(value, url);
if (row.part_detail.is_template) {
html += `<span class='fas fa-clone float-right' title='{% trans "Template part" %}'></span>`;
html += makeIconBadge('fa-clone', '{% trans "Template part" %}');
}
if (row.part_detail.assembly) {
html += `<span class='fas fa-tools float-right' title='{% trans "Assembled part" %}'></span>`;
html += makeIconBadge('fa-tools', '{% trans "Assembled part" %}');
}
if (!row.part_detail.active) {
@ -764,16 +980,13 @@ function loadManufacturerPartTable(table, url, options) {
sortable: false,
switchable: false,
formatter: function(value, row) {
var pk = row.pk;
let pk = row.pk;
let html = '';
var html = `<div class='btn-group float-right' role='group'>`;
html += makeEditButton('button-manufacturer-part-edit', pk, '{% trans "Edit manufacturer part" %}');
html += makeDeleteButton('button-manufacturer-part-delete', pk, '{% trans "Delete manufacturer part" %}');
html += makeIconButton('fa-edit icon-blue', 'button-manufacturer-part-edit', pk, '{% trans "Edit manufacturer part" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-manufacturer-part-delete', pk, '{% trans "Delete manufacturer part" %}');
html += '</div>';
return html;
return wrapButtons(html);
}
}
],
@ -810,20 +1023,15 @@ function loadManufacturerPartTable(table, url, options) {
}
/*
* Load table of ManufacturerPartParameter objects
*/
function loadManufacturerPartParameterTable(table, url, options) {
/*
* Load table of ManufacturerPartParameter objects
*/
var params = options.params || {};
// Load filters
var filters = loadTableFilters('manufacturer-part-parameters');
// Overwrite explicit parameters
for (var key in params) {
filters[key] = params[key];
}
var filters = loadTableFilters('manufacturer-part-parameters', params);
setupFilterList('manufacturer-part-parameters', $(table));
@ -867,17 +1075,13 @@ function loadManufacturerPartParameterTable(table, url, options) {
switchable: false,
sortable: false,
formatter: function(value, row) {
let pk = row.pk;
let html = '';
var pk = row.pk;
html += makeEditButton('button-parameter-edit', pk, '{% trans "Edit parameter" %}');
html += makeDeleteButton('button-parameter-delete', pk, '{% trans "Delete parameter" %}');
var html = `<div class='btn-group float-right' role='group'>`;
html += makeIconButton('fa-edit icon-blue', 'button-parameter-edit', pk, '{% trans "Edit parameter" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-parameter-delete', pk, '{% trans "Delete parameter" %}');
html += `</div>`;
return html;
return wrapButtons(html);
}
}
],
@ -886,27 +1090,23 @@ function loadManufacturerPartParameterTable(table, url, options) {
$(table).find('.button-parameter-edit').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/company/part/manufacturer/parameter/${pk}/`, {
constructForm(`{% url "api-manufacturer-part-parameter-list" %}${pk}/`, {
fields: {
name: {},
value: {},
units: {},
},
title: '{% trans "Edit Parameter" %}',
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
refreshTable: table,
});
});
$(table).find('.button-parameter-delete').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/company/part/manufacturer/parameter/${pk}/`, {
constructForm(`{% url "api-manufacturer-part-parameter-list" %}${pk}/`, {
method: 'DELETE',
title: '{% trans "Delete Parameter" %}',
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
refreshTable: table,
});
});
}
@ -914,21 +1114,16 @@ function loadManufacturerPartParameterTable(table, url, options) {
}
/*
* Load supplier part table
*/
function loadSupplierPartTable(table, url, options) {
/*
* Load supplier part table
*
*/
// Query parameters
var params = options.params || {};
// Load filters
var filters = loadTableFilters('supplier-part');
for (var key in params) {
filters[key] = params[key];
}
var filters = loadTableFilters('supplier-part', params);
setupFilterList('supplier-part', $(table));
@ -964,11 +1159,11 @@ function loadSupplierPartTable(table, url, options) {
var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(value, url);
if (row.part_detail.is_template) {
html += `<span class='fas fa-clone float-right' title='{% trans "Template part" %}'></span>`;
html += makeIconBadge('fa-clone', '{% trans "Template part" %}');
}
if (row.part_detail.assembly) {
html += `<span class='fas fa-tools float-right' title='{% trans "Assembled part" %}'></span>`;
html += makeIconBadge('fa-tools', '{% trans "Assembled part" %}');
}
if (!row.part_detail.active) {
@ -1088,9 +1283,13 @@ function loadSupplierPartTable(table, url, options) {
sortable: true,
formatter: function(value, row) {
if (row.availability_updated) {
var html = formatDecimal(value);
var date = renderDate(row.availability_updated, {showTime: true});
html += `<span class='fas fa-info-circle float-right' title='{% trans "Last Updated" %}: ${date}'></span>`;
let html = formatDecimal(value);
let date = renderDate(row.availability_updated, {showTime: true});
html += makeIconBadge(
'fa-info-circle',
`{% trans "Last Updated" %}: ${date}`
);
return html;
} else {
return '-';
@ -1108,16 +1307,13 @@ function loadSupplierPartTable(table, url, options) {
sortable: false,
switchable: false,
formatter: function(value, row) {
var pk = row.pk;
let pk = row.pk;
let html = '';
var html = `<div class='btn-group float-right' role='group'>`;
html += makeEditButton('button-supplier-part-edit', pk, '{% trans "Edit supplier part" %}');
html += makeDeleteButton('button-supplier-part-delete', pk, '{% trans "Delete supplier part" %}');
html += makeIconButton('fa-edit icon-blue', 'button-supplier-part-edit', pk, '{% trans "Edit supplier part" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-supplier-part-delete', pk, '{% trans "Delete supplier part" %}');
html += '</div>';
return html;
return wrapButtons(html);
}
}
],
@ -1166,24 +1362,20 @@ function loadSupplierPriceBreakTable(options={}) {
table.find('.button-price-break-delete').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/company/price-break/${pk}/`, {
constructForm(`{% url "api-part-supplier-price-list" %}${pk}/`, {
method: 'DELETE',
title: '{% trans "Delete Price Break" %}',
onSuccess: function() {
table.bootstrapTable('refresh');
},
refreshTable: table,
});
});
table.find('.button-price-break-edit').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/company/price-break/${pk}/`, {
constructForm(`{% url "api-part-supplier-price-list" %}${pk}/`, {
fields: supplierPartPriceBreakFields(),
title: '{% trans "Edit Price Break" %}',
onSuccess: function() {
table.bootstrapTable('refresh');
}
refreshTable: table,
});
});
}
@ -1231,10 +1423,12 @@ function loadSupplierPriceBreakTable(options={}) {
formatter: function(value, row) {
var html = renderDate(value);
html += `<div class='btn-group float-right' role='group'>`;
html += makeIconButton('fa-edit icon-blue', 'button-price-break-edit', row.pk, '{% trans "Edit price break" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-price-break-delete', row.pk, '{% trans "Delete price break" %}');
html += `</div>`;
let buttons = '';
buttons += makeEditButton('button-price-break-edit', row.pk, '{% trans "Edit price break" %}');
buttons += makeDeleteButton('button-price-break-delete', row.pk, '{% trans "Delete price break" %}');
html += wrapButtons(buttons);
return html;
}