Skip to content

Commit fe4cbe0

Browse files
authored
Merge branch 'master' into devOps
2 parents f98ec6c + 8385080 commit fe4cbe0

30 files changed

Lines changed: 851 additions & 39 deletions

File tree

.github/workflows/qc_checks.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ jobs:
153153
invoke delete-data -f
154154
invoke import-fixtures
155155
invoke server -a 127.0.0.1:12345 &
156+
invoke wait
156157
- name: Run Tests
157158
run: |
158159
cd ${{ env.wrapper_name }}

InvenTree/InvenTree/api_tester.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
Helper functions for performing API unit tests
33
"""
44

5+
import csv
6+
import io
7+
import re
8+
9+
from django.http.response import StreamingHttpResponse
510
from django.contrib.auth import get_user_model
611
from django.contrib.auth.models import Group
712
from rest_framework.test import APITestCase
@@ -165,3 +170,87 @@ def options(self, url, expected_code=None):
165170
self.assertEqual(response.status_code, expected_code)
166171

167172
return response
173+
174+
def download_file(self, url, data, expected_code=None, expected_fn=None, decode=True):
175+
"""
176+
Download a file from the server, and return an in-memory file
177+
"""
178+
179+
response = self.client.get(url, data=data, format='json')
180+
181+
if expected_code is not None:
182+
self.assertEqual(response.status_code, expected_code)
183+
184+
# Check that the response is of the correct type
185+
if not isinstance(response, StreamingHttpResponse):
186+
raise ValueError("Response is not a StreamingHttpResponse object as expected")
187+
188+
# Extract filename
189+
disposition = response.headers['Content-Disposition']
190+
191+
result = re.search(r'attachment; filename="([\w.]+)"', disposition)
192+
193+
fn = result.groups()[0]
194+
195+
if expected_fn is not None:
196+
self.assertEqual(expected_fn, fn)
197+
198+
if decode:
199+
# Decode data and return as StringIO file object
200+
fo = io.StringIO()
201+
fo.name = fo
202+
fo.write(response.getvalue().decode('UTF-8'))
203+
else:
204+
# Return a a BytesIO file object
205+
fo = io.BytesIO()
206+
fo.name = fn
207+
fo.write(response.getvalue())
208+
209+
fo.seek(0)
210+
211+
return fo
212+
213+
def process_csv(self, fo, delimiter=',', required_cols=None, excluded_cols=None, required_rows=None):
214+
"""
215+
Helper function to process and validate a downloaded csv file
216+
"""
217+
218+
# Check that the correct object type has been passed
219+
self.assertTrue(isinstance(fo, io.StringIO))
220+
221+
fo.seek(0)
222+
223+
reader = csv.reader(fo, delimiter=delimiter)
224+
225+
headers = []
226+
rows = []
227+
228+
for idx, row in enumerate(reader):
229+
if idx == 0:
230+
headers = row
231+
else:
232+
rows.append(row)
233+
234+
if required_cols is not None:
235+
for col in required_cols:
236+
self.assertIn(col, headers)
237+
238+
if excluded_cols is not None:
239+
for col in excluded_cols:
240+
self.assertNotIn(col, headers)
241+
242+
if required_rows is not None:
243+
self.assertEqual(len(rows), required_rows)
244+
245+
# Return the file data as a list of dict items, based on the headers
246+
data = []
247+
248+
for row in rows:
249+
entry = {}
250+
251+
for idx, col in enumerate(headers):
252+
entry[col] = row[idx]
253+
254+
data.append(entry)
255+
256+
return data

InvenTree/InvenTree/ready.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ def canAppAccessDatabase(allow_test=False):
3939
'createsuperuser',
4040
'wait_for_db',
4141
'prerender',
42-
'rebuild',
42+
'rebuild_models',
43+
'rebuild_thumbnails',
4344
'collectstatic',
4445
'makemessages',
4546
'compilemessages',

InvenTree/InvenTree/tests.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import os
3+
import time
34

45
from unittest import mock
56

@@ -406,11 +407,23 @@ def test_rates(self):
406407
with self.assertRaises(MissingRate):
407408
convert_money(Money(100, 'AUD'), 'USD')
408409

409-
InvenTree.tasks.update_exchange_rates()
410+
update_successful = False
410411

411-
rates = Rate.objects.all()
412+
# Note: the update sometimes fails in CI, let's give it a few chances
413+
for idx in range(10):
414+
InvenTree.tasks.update_exchange_rates()
415+
416+
rates = Rate.objects.all()
417+
418+
if rates.count() == len(currency_codes()):
419+
update_successful = True
420+
break
421+
422+
else:
423+
print("Exchange rate update failed - retrying")
424+
time.sleep(1)
412425

413-
self.assertEqual(rates.count(), len(currency_codes()))
426+
self.assertTrue(update_successful)
414427

415428
# Now that we have some exchange rate information, we can perform conversions
416429

InvenTree/build/admin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class BuildResource(ModelResource):
1616
# but we don't for other ones.
1717
# TODO: 2022-05-12 - Need to investigate why this is the case!
1818

19-
pk = Field(attribute='pk')
19+
id = Field(attribute='pk')
2020

2121
reference = Field(attribute='reference')
2222

@@ -45,6 +45,7 @@ class Meta:
4545
clean_model_instances = True
4646
exclude = [
4747
'lft', 'rght', 'tree_id', 'level',
48+
'metadata',
4849
]
4950

5051

InvenTree/build/test_api.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,50 @@ def test_create_delete_output(self):
511511

512512
self.assertIn('This build output has already been completed', str(response.data))
513513

514+
def test_download_build_orders(self):
515+
516+
required_cols = [
517+
'reference',
518+
'status',
519+
'completed',
520+
'batch',
521+
'notes',
522+
'title',
523+
'part',
524+
'part_name',
525+
'id',
526+
'quantity',
527+
]
528+
529+
excluded_cols = [
530+
'lft', 'rght', 'tree_id', 'level',
531+
'metadata',
532+
]
533+
534+
with self.download_file(
535+
reverse('api-build-list'),
536+
{
537+
'export': 'csv',
538+
}
539+
) as fo:
540+
541+
data = self.process_csv(
542+
fo,
543+
required_cols=required_cols,
544+
excluded_cols=excluded_cols,
545+
required_rows=Build.objects.count()
546+
)
547+
548+
for row in data:
549+
550+
build = Build.objects.get(pk=row['id'])
551+
552+
self.assertEqual(str(build.part.pk), row['part'])
553+
self.assertEqual(build.part.full_name, row['part_name'])
554+
555+
self.assertEqual(build.reference, row['reference'])
556+
self.assertEqual(build.title, row['title'])
557+
514558

515559
class BuildAllocationTest(BuildAPITest):
516560
"""

InvenTree/common/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,13 @@ def save(self, *args, **kwargs):
11111111
'default': 'SO',
11121112
},
11131113

1114+
'SALESORDER_DEFAULT_SHIPMENT': {
1115+
'name': _('Sales Order Default Shipment'),
1116+
'description': _('Enable creation of default shipment with sales orders'),
1117+
'default': False,
1118+
'validator': bool,
1119+
},
1120+
11141121
'PURCHASEORDER_REFERENCE_PREFIX': {
11151122
'name': _('Purchase Order Reference Prefix'),
11161123
'description': _('Prefix value for purchase order reference'),

InvenTree/common/tests.py

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -112,28 +112,61 @@ def test_allValues(self):
112112
self.assertIn('STOCK_OWNERSHIP_CONTROL', result)
113113
self.assertIn('SIGNUP_GROUP', result)
114114

115-
def test_required_values(self):
116-
"""
117-
- Ensure that every global setting has a name.
118-
- Ensure that every global setting has a description.
119-
"""
115+
def run_settings_check(self, key, setting):
120116

121-
for key in InvenTreeSetting.SETTINGS.keys():
117+
self.assertTrue(type(setting) is dict)
118+
119+
name = setting.get('name', None)
120+
121+
self.assertIsNotNone(name)
122+
self.assertIn('django.utils.functional.lazy', str(type(name)))
123+
124+
description = setting.get('description', None)
125+
126+
self.assertIsNotNone(description)
127+
self.assertIn('django.utils.functional.lazy', str(type(description)))
122128

123-
setting = InvenTreeSetting.SETTINGS[key]
129+
if key != key.upper():
130+
raise ValueError(f"Setting key '{key}' is not uppercase") # pragma: no cover
124131

125-
name = setting.get('name', None)
132+
# Check that only allowed keys are provided
133+
allowed_keys = [
134+
'name',
135+
'description',
136+
'default',
137+
'validator',
138+
'hidden',
139+
'choices',
140+
'units',
141+
'requires_restart',
142+
]
126143

127-
if name is None:
128-
raise ValueError(f'Missing GLOBAL_SETTING name for {key}') # pragma: no cover
144+
for k in setting.keys():
145+
self.assertIn(k, allowed_keys)
129146

130-
description = setting.get('description', None)
147+
# Check default value for boolean settings
148+
validator = setting.get('validator', None)
149+
150+
if validator is bool:
151+
default = setting.get('default', None)
152+
153+
# Default value *must* be supplied for boolean setting!
154+
self.assertIsNotNone(default)
155+
156+
# Default value for boolean must itself be a boolean
157+
self.assertIn(default, [True, False])
158+
159+
def test_setting_data(self):
160+
"""
161+
- Ensure that every setting has a name, which is translated
162+
- Ensure that every setting has a description, which is translated
163+
"""
131164

132-
if description is None:
133-
raise ValueError(f'Missing GLOBAL_SETTING description for {key}') # pragma: no cover
165+
for key, setting in InvenTreeSetting.SETTINGS.items():
166+
self.run_settings_check(key, setting)
134167

135-
if key != key.upper():
136-
raise ValueError(f"SETTINGS key '{key}' is not uppercase") # pragma: no cover
168+
for key, setting in InvenTreeUserSetting.SETTINGS.items():
169+
self.run_settings_check(key, setting)
137170

138171
def test_defaults(self):
139172
"""

InvenTree/company/templates/company/manufacturer_part.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,6 @@ <h4>
9494
{% else %}
9595
<em>{% trans "No manufacturer information available" %}</em>
9696
{% endif %}
97-
{% endif %}
9897
</td>
9998
</tr>
10099
<tr>

InvenTree/company/test_views.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,29 @@ def test_company_index(self):
5555

5656
response = self.client.get(reverse('company-index'))
5757
self.assertEqual(response.status_code, 200)
58+
59+
def test_manufacturer_index(self):
60+
""" Test the manufacturer index """
61+
62+
response = self.client.get(reverse('manufacturer-index'))
63+
self.assertEqual(response.status_code, 200)
64+
65+
def test_customer_index(self):
66+
""" Test the customer index """
67+
68+
response = self.client.get(reverse('customer-index'))
69+
self.assertEqual(response.status_code, 200)
70+
71+
def test_manufacturer_part_detail_view(self):
72+
""" Test the manufacturer part detail view """
73+
74+
response = self.client.get(reverse('manufacturer-part-detail', kwargs={'pk': 1}))
75+
self.assertEqual(response.status_code, 200)
76+
self.assertContains(response, 'MPN123')
77+
78+
def test_supplier_part_detail_view(self):
79+
""" Test the supplier part detail view """
80+
81+
response = self.client.get(reverse('supplier-part-detail', kwargs={'pk': 10}))
82+
self.assertEqual(response.status_code, 200)
83+
self.assertContains(response, 'MPN456-APPEL')

0 commit comments

Comments
 (0)