2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-09-18 08:31:33 +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

@@ -17,7 +17,9 @@ import order.models as models
from common.settings import currency_codes
from company.models import Company
from InvenTree.api_tester import InvenTreeAPITestCase
from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus
from InvenTree.status_codes import (PurchaseOrderStatus, ReturnOrderLineStatus,
ReturnOrderStatus, SalesOrderStatus,
StockStatus)
from part.models import Part
from stock.models import StockItem
@@ -1802,3 +1804,286 @@ class SalesOrderAllocateTest(OrderTest):
response = self.get(url, expected_code=200)
self.assertEqual(len(response.data), 1 + 3 * models.SalesOrder.objects.count())
class ReturnOrderTests(InvenTreeAPITestCase):
"""Unit tests for ReturnOrder API endpoints"""
fixtures = [
'category',
'company',
'return_order',
'part',
'location',
'supplier_part',
'stock',
]
def test_options(self):
"""Test the OPTIONS endpoint"""
self.assignRole('return_order.add')
data = self.options(reverse('api-return-order-list'), expected_code=200).data
self.assertEqual(data['name'], 'Return Order List')
# Some checks on the 'reference' field
post = data['actions']['POST']
reference = post['reference']
self.assertEqual(reference['default'], 'RMA-0007')
self.assertEqual(reference['label'], 'Reference')
self.assertEqual(reference['help_text'], 'Return Order reference')
self.assertEqual(reference['required'], True)
self.assertEqual(reference['type'], 'string')
def test_list(self):
"""Tests for the list endpoint"""
url = reverse('api-return-order-list')
response = self.get(url, expected_code=200)
self.assertEqual(len(response.data), 6)
# Paginated query
data = self.get(
url,
{
'limit': 1,
'ordering': 'reference',
'customer_detail': True,
},
expected_code=200
).data
self.assertEqual(data['count'], 6)
self.assertEqual(len(data['results']), 1)
result = data['results'][0]
self.assertEqual(result['reference'], 'RMA-001')
self.assertEqual(result['customer_detail']['name'], 'A customer')
# Reverse ordering
data = self.get(
url,
{
'ordering': '-reference',
},
expected_code=200
).data
self.assertEqual(data[0]['reference'], 'RMA-006')
# Filter by customer
for cmp_id in [4, 5]:
data = self.get(
url,
{
'customer': cmp_id,
},
expected_code=200
).data
self.assertEqual(len(data), 3)
for result in data:
self.assertEqual(result['customer'], cmp_id)
# Filter by status
data = self.get(
url,
{
'status': 20,
},
expected_code=200
).data
self.assertEqual(len(data), 2)
for result in data:
self.assertEqual(result['status'], 20)
def test_create(self):
"""Test creation of ReturnOrder via the API"""
url = reverse('api-return-order-list')
# Do not have required permissions yet
self.post(
url,
{
'customer': 1,
'description': 'a return order',
},
expected_code=403
)
self.assignRole('return_order.add')
data = self.post(
url,
{
'customer': 4,
'customer_reference': 'cr',
'description': 'a return order',
},
expected_code=201
).data
# Reference automatically generated
self.assertEqual(data['reference'], 'RMA-0007')
self.assertEqual(data['customer_reference'], 'cr')
def test_update(self):
"""Test that we can update a ReturnOrder via the API"""
url = reverse('api-return-order-detail', kwargs={'pk': 1})
# Test detail endpoint
data = self.get(url, expected_code=200).data
self.assertEqual(data['reference'], 'RMA-001')
# Attempt to update, incorrect permissions
self.patch(
url,
{
'customer_reference': 'My customer reference',
},
expected_code=403
)
self.assignRole('return_order.change')
self.patch(
url,
{
'customer_reference': 'customer ref',
},
expected_code=200
)
rma = models.ReturnOrder.objects.get(pk=1)
self.assertEqual(rma.customer_reference, 'customer ref')
def test_ro_issue(self):
"""Test the 'issue' order for a ReturnOrder"""
order = models.ReturnOrder.objects.get(pk=1)
self.assertEqual(order.status, ReturnOrderStatus.PENDING)
self.assertIsNone(order.issue_date)
url = reverse('api-return-order-issue', kwargs={'pk': 1})
# POST without required permissions
self.post(url, expected_code=403)
self.assignRole('return_order.add')
self.post(url, expected_code=201)
order.refresh_from_db()
self.assertEqual(order.status, ReturnOrderStatus.IN_PROGRESS)
self.assertIsNotNone(order.issue_date)
def test_receive(self):
"""Test that we can receive items against a ReturnOrder"""
customer = Company.objects.get(pk=4)
# Create an order
rma = models.ReturnOrder.objects.create(
customer=customer,
description='A return order',
)
self.assertEqual(rma.reference, 'RMA-0007')
# Create some line items
part = Part.objects.get(pk=25)
for idx in range(3):
stock_item = StockItem.objects.create(
part=part, customer=customer,
quantity=1, serial=idx
)
line_item = models.ReturnOrderLineItem.objects.create(
order=rma,
item=stock_item,
)
self.assertEqual(line_item.outcome, ReturnOrderLineStatus.PENDING)
self.assertIsNone(line_item.received_date)
self.assertFalse(line_item.received)
self.assertEqual(rma.lines.count(), 3)
def receive(items, location=None, expected_code=400):
"""Helper function to receive items against this ReturnOrder"""
url = reverse('api-return-order-receive', kwargs={'pk': rma.pk})
response = self.post(
url,
{
'items': items,
'location': location,
},
expected_code=expected_code
)
return response.data
# Receive without required permissions
receive([], expected_code=403)
self.assignRole('return_order.add')
# Receive, without any location
data = receive([], expected_code=400)
self.assertIn('This field may not be null', str(data['location']))
# Receive, with incorrect order code
data = receive([], 1, expected_code=400)
self.assertIn('Items can only be received against orders which are in progress', str(data))
# Issue the order (via the API)
self.assertIsNone(rma.issue_date)
self.post(
reverse("api-return-order-issue", kwargs={"pk": rma.pk}),
expected_code=201,
)
rma.refresh_from_db()
self.assertIsNotNone(rma.issue_date)
self.assertEqual(rma.status, ReturnOrderStatus.IN_PROGRESS)
# Receive, without any items
data = receive([], 1, expected_code=400)
self.assertIn('Line items must be provided', str(data))
# Get a reference to one of the stock items
stock_item = rma.lines.first().item
n_tracking = stock_item.tracking_info.count()
# Receive items successfully
data = receive(
[{'item': line.pk} for line in rma.lines.all()],
1,
expected_code=201
)
# Check that all line items have been received
for line in rma.lines.all():
self.assertTrue(line.received)
self.assertIsNotNone(line.received_date)
# A single tracking entry should have been added to the item
self.assertEqual(stock_item.tracking_info.count(), n_tracking + 1)
tracking_entry = stock_item.tracking_info.last()
deltas = tracking_entry.deltas
self.assertEqual(deltas['status'], StockStatus.QUARANTINED)
self.assertEqual(deltas['customer'], customer.pk)
self.assertEqual(deltas['location'], 1)
self.assertEqual(deltas['returnorder'], rma.pk)