django-bom 1.235__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-bom might be problematic. Click here for more details.

Files changed (182) hide show
  1. bom/__init__.py +1 -0
  2. bom/admin.py +161 -0
  3. bom/apps.py +8 -0
  4. bom/base_classes.py +31 -0
  5. bom/constants.py +210 -0
  6. bom/context_processors.py +9 -0
  7. bom/csv_headers.py +274 -0
  8. bom/decorators.py +32 -0
  9. bom/form_fields.py +59 -0
  10. bom/forms.py +1400 -0
  11. bom/helpers.py +292 -0
  12. bom/local_settings.py +36 -0
  13. bom/migrations/0001_initial.py +135 -0
  14. bom/migrations/0002_auto_20180908_2151.py +24 -0
  15. bom/migrations/0003_sellerpart_data_source.py +18 -0
  16. bom/migrations/0004_auto_20180911_0011.py +18 -0
  17. bom/migrations/0005_auto_20181007_1934.py +56 -0
  18. bom/migrations/0006_auto_20181007_1949.py +41 -0
  19. bom/migrations/0007_auto_20181009_0256.py +19 -0
  20. bom/migrations/0008_auto_20181030_0427.py +19 -0
  21. bom/migrations/0009_subpart_reference.py +18 -0
  22. bom/migrations/0010_auto_20181202_0733.py +23 -0
  23. bom/migrations/0011_auto_20181202_2113.py +22 -0
  24. bom/migrations/0012_partchangehistory.py +30 -0
  25. bom/migrations/0013_auto_20190222_1631.py +19 -0
  26. bom/migrations/0014_auto_20190223_2353.py +18 -0
  27. bom/migrations/0015_auto_20190303_1915.py +136 -0
  28. bom/migrations/0016_auto_20190405_2308.py +58 -0
  29. bom/migrations/0017_auto_20190616_1912.py +19 -0
  30. bom/migrations/0018_auto_20190616_2143.py +24 -0
  31. bom/migrations/0019_auto_20190624_1246.py +45 -0
  32. bom/migrations/0020_auto_20190627_0207.py +38 -0
  33. bom/migrations/0021_auto_20190627_0428.py +23 -0
  34. bom/migrations/0022_auto_20190811_2140.py +35 -0
  35. bom/migrations/0023_auto_20191205_2351.py +255 -0
  36. bom/migrations/0024_auto_20191214_1342.py +89 -0
  37. bom/migrations/0025_auto_20191221_1907.py +38 -0
  38. bom/migrations/0026_auto_20191222_2258.py +22 -0
  39. bom/migrations/0027_auto_20191222_2347.py +17 -0
  40. bom/migrations/0028_partrevision_displayable_synopsis.py +74 -0
  41. bom/migrations/0029_auto_20191231_1630.py +23 -0
  42. bom/migrations/0030_auto_20200101_2253.py +22 -0
  43. bom/migrations/0031_auto_20200104_1352.py +38 -0
  44. bom/migrations/0032_auto_20200126_1806.py +27 -0
  45. bom/migrations/0033_auto_20200203_0618.py +29 -0
  46. bom/migrations/0034_auto_20200222_0359.py +30 -0
  47. bom/migrations/0035_auto_20200303_0111.py +34 -0
  48. bom/migrations/0036_auto_20200303_0538.py +17 -0
  49. bom/migrations/0037_auto_20200405_1642.py +44 -0
  50. bom/migrations/0038_auto_20200422_0504.py +19 -0
  51. bom/migrations/0039_auto_20200929_2315.py +41 -0
  52. bom/migrations/0040_alter_organization_currency.py +19 -0
  53. bom/migrations/0041_organization_subscription_quantity.py +18 -0
  54. bom/migrations/0042_auto_20210720_2137.py +23 -0
  55. bom/migrations/0043_auto_20211123_0157.py +24 -0
  56. bom/migrations/0044_auto_20220831_1241.py +23 -0
  57. bom/migrations/0045_sellerpart_link.py +18 -0
  58. bom/migrations/0046_alter_sellerpart_unique_together.py +17 -0
  59. bom/migrations/0047_sellerpart_seller_part_number.py +18 -0
  60. bom/migrations/0048_rename_part_organization_number_class_bom_part_organiz_b333d6_idx_and_more.py +1017 -0
  61. bom/migrations/__init__.py +0 -0
  62. bom/models.py +756 -0
  63. bom/part_bom.py +192 -0
  64. bom/settings.py +225 -0
  65. bom/static/bom/css/dashboard.css +17 -0
  66. bom/static/bom/css/jquery.treetable.css +28 -0
  67. bom/static/bom/css/materialize.min.css +13 -0
  68. bom/static/bom/css/part-info.css +15 -0
  69. bom/static/bom/css/style.css +308 -0
  70. bom/static/bom/css/tablesorter-theme.materialize.css +176 -0
  71. bom/static/bom/css/treetable-theme.css +42 -0
  72. bom/static/bom/doc/sample_part_classes.csv +38 -0
  73. bom/static/bom/doc/test_bom.csv +6 -0
  74. bom/static/bom/doc/test_bom_5_intelligent.csv +4 -0
  75. bom/static/bom/doc/test_full_bom.csv +37 -0
  76. bom/static/bom/doc/test_new_parts.csv +5 -0
  77. bom/static/bom/doc/test_new_parts_5_intelligent.csv +5 -0
  78. bom/static/bom/img/_ionicons_svg_md-arrow-dropdown.svg +1 -0
  79. bom/static/bom/img/_ionicons_svg_md-arrow-dropright.svg +1 -0
  80. bom/static/bom/img/favicon.ico +0 -0
  81. bom/static/bom/img/google/web/1x/btn_google_signin_dark_disabled_web.png +0 -0
  82. bom/static/bom/img/google/web/1x/btn_google_signin_dark_focus_web.png +0 -0
  83. bom/static/bom/img/google/web/1x/btn_google_signin_dark_normal_web.png +0 -0
  84. bom/static/bom/img/google/web/1x/btn_google_signin_dark_pressed_web.png +0 -0
  85. bom/static/bom/img/google/web/1x/btn_google_signin_light_disabled_web.png +0 -0
  86. bom/static/bom/img/google/web/1x/btn_google_signin_light_focus_web.png +0 -0
  87. bom/static/bom/img/google/web/1x/btn_google_signin_light_normal_web.png +0 -0
  88. bom/static/bom/img/google/web/1x/btn_google_signin_light_pressed_web.png +0 -0
  89. bom/static/bom/img/google/web/2x/btn_google_signin_dark_disabled_web@2x.png +0 -0
  90. bom/static/bom/img/google/web/2x/btn_google_signin_dark_focus_web@2x.png +0 -0
  91. bom/static/bom/img/google/web/2x/btn_google_signin_dark_normal_web@2x.png +0 -0
  92. bom/static/bom/img/google/web/2x/btn_google_signin_dark_pressed_web@2x.png +0 -0
  93. bom/static/bom/img/google/web/2x/btn_google_signin_light_disabled_web@2x.png +0 -0
  94. bom/static/bom/img/google/web/2x/btn_google_signin_light_focus_web@2x.png +0 -0
  95. bom/static/bom/img/google/web/2x/btn_google_signin_light_normal_web@2x.png +0 -0
  96. bom/static/bom/img/google/web/2x/btn_google_signin_light_pressed_web@2x.png +0 -0
  97. bom/static/bom/img/google/web/vector/btn_google_dark_disabled_ios.eps +814 -0
  98. bom/static/bom/img/google/web/vector/btn_google_dark_disabled_ios.svg +24 -0
  99. bom/static/bom/img/google/web/vector/btn_google_dark_focus_ios.eps +1866 -0
  100. bom/static/bom/img/google/web/vector/btn_google_dark_focus_ios.svg +51 -0
  101. bom/static/bom/img/google/web/vector/btn_google_dark_normal_ios.eps +1031 -0
  102. bom/static/bom/img/google/web/vector/btn_google_dark_normal_ios.svg +50 -0
  103. bom/static/bom/img/google/web/vector/btn_google_dark_pressed_ios.eps +1031 -0
  104. bom/static/bom/img/google/web/vector/btn_google_dark_pressed_ios.svg +50 -0
  105. bom/static/bom/img/google/web/vector/btn_google_light_disabled_ios.eps +814 -0
  106. bom/static/bom/img/google/web/vector/btn_google_light_disabled_ios.svg +24 -0
  107. bom/static/bom/img/google/web/vector/btn_google_light_focus_ios.eps +1837 -0
  108. bom/static/bom/img/google/web/vector/btn_google_light_focus_ios.svg +44 -0
  109. bom/static/bom/img/google/web/vector/btn_google_light_normal_ios.eps +1002 -0
  110. bom/static/bom/img/google/web/vector/btn_google_light_normal_ios.svg +43 -0
  111. bom/static/bom/img/google/web/vector/btn_google_light_pressed_ios.eps +1002 -0
  112. bom/static/bom/img/google/web/vector/btn_google_light_pressed_ios.svg +43 -0
  113. bom/static/bom/img/google_drive_logo.svg +1 -0
  114. bom/static/bom/img/indabom.png +0 -0
  115. bom/static/bom/img/mouser.png +0 -0
  116. bom/static/bom/img/octopart_blue.svg +19 -0
  117. bom/static/bom/js/jquery-3.4.1.min.js +2 -0
  118. bom/static/bom/js/jquery.ba-floatingscrollbar.min.js +10 -0
  119. bom/static/bom/js/jquery.treetable.js +629 -0
  120. bom/static/bom/js/materialize.min.js +6 -0
  121. bom/templates/bom/account-delete.html +23 -0
  122. bom/templates/bom/add-manufacturer-part.html +69 -0
  123. bom/templates/bom/add-sellerpart.html +96 -0
  124. bom/templates/bom/base-menu.html +7 -0
  125. bom/templates/bom/base.html +129 -0
  126. bom/templates/bom/bom-action-btn.html +23 -0
  127. bom/templates/bom/bom-action-table.html +57 -0
  128. bom/templates/bom/bom-base-menu.html +6 -0
  129. bom/templates/bom/bom-base.html +24 -0
  130. bom/templates/bom/bom-form-modal.html +35 -0
  131. bom/templates/bom/bom-form.html +34 -0
  132. bom/templates/bom/bom-signup.html +16 -0
  133. bom/templates/bom/components/bom-flat.html +131 -0
  134. bom/templates/bom/components/bom-indented.html +237 -0
  135. bom/templates/bom/components/manufacturer-part-list.html +271 -0
  136. bom/templates/bom/components/seller-part-list.html +63 -0
  137. bom/templates/bom/create-part.html +68 -0
  138. bom/templates/bom/dashboard-menu.html +15 -0
  139. bom/templates/bom/dashboard.html +300 -0
  140. bom/templates/bom/edit-manufacturer-part.html +75 -0
  141. bom/templates/bom/edit-part-class.html +36 -0
  142. bom/templates/bom/edit-part.html +71 -0
  143. bom/templates/bom/edit-user-meta.html +41 -0
  144. bom/templates/bom/help.html +1363 -0
  145. bom/templates/bom/manufacturer-info.html +83 -0
  146. bom/templates/bom/manufacturers.html +96 -0
  147. bom/templates/bom/nothing-to-see.html +15 -0
  148. bom/templates/bom/organization-create.html +135 -0
  149. bom/templates/bom/part-info.html +440 -0
  150. bom/templates/bom/part-revision-display.html +184 -0
  151. bom/templates/bom/part-revision-edit.html +42 -0
  152. bom/templates/bom/part-revision-manage-bom.html +116 -0
  153. bom/templates/bom/part-revision-new.html +60 -0
  154. bom/templates/bom/part-revision-release.html +48 -0
  155. bom/templates/bom/search-help.html +105 -0
  156. bom/templates/bom/seller-info.html +83 -0
  157. bom/templates/bom/sellers.html +96 -0
  158. bom/templates/bom/settings.html +405 -0
  159. bom/templates/bom/signup.html +28 -0
  160. bom/templates/bom/table_of_contents.html +47 -0
  161. bom/templates/bom/upload-bom.html +112 -0
  162. bom/templates/bom/upload-parts-help.html +107 -0
  163. bom/templates/bom/upload-parts.html +54 -0
  164. bom/templates/registration/login.html +39 -0
  165. bom/tests.py +1506 -0
  166. bom/third_party_apis/__init__.py +0 -0
  167. bom/third_party_apis/base_api.py +51 -0
  168. bom/third_party_apis/google_drive.py +166 -0
  169. bom/third_party_apis/mouser.py +132 -0
  170. bom/third_party_apis/test_apis.py +24 -0
  171. bom/urls.py +89 -0
  172. bom/utils.py +228 -0
  173. bom/validators.py +23 -0
  174. bom/views/__init__.py +0 -0
  175. bom/views/json_views.py +55 -0
  176. bom/views/views.py +1620 -0
  177. bom/wsgi.py +16 -0
  178. django_bom-1.235.dist-info/METADATA +207 -0
  179. django_bom-1.235.dist-info/RECORD +182 -0
  180. django_bom-1.235.dist-info/WHEEL +5 -0
  181. django_bom-1.235.dist-info/licenses/LICENSE +674 -0
  182. django_bom-1.235.dist-info/top_level.txt +1 -0
bom/tests.py ADDED
@@ -0,0 +1,1506 @@
1
+ import csv
2
+ from re import finditer, search
3
+ from unittest import skip
4
+
5
+ from django.conf import settings
6
+ from django.contrib.auth.models import User
7
+ from django.test import Client, TestCase, TransactionTestCase, override_settings
8
+ from django.urls import reverse
9
+
10
+ from . import constants
11
+ from .forms import AddSubpartForm, PartFormSemiIntelligent, PartInfoForm, SellerPartForm
12
+ from .helpers import (
13
+ create_a_fake_assembly,
14
+ create_a_fake_organization,
15
+ create_a_fake_part_revision,
16
+ create_a_fake_subpart,
17
+ create_some_fake_manufacturers,
18
+ create_some_fake_part_classes,
19
+ create_some_fake_parts,
20
+ create_some_fake_sellers,
21
+ create_user_and_organization,
22
+ )
23
+ from .models import ManufacturerPart, Part, PartClass, Seller, SellerPart, Subpart
24
+
25
+
26
+ TEST_FILES_DIR = "bom/test_files"
27
+
28
+ @override_settings(BOM_CONFIG=settings.BOM_CONFIG_DEFAULT)
29
+ class TestBomAuth(TransactionTestCase):
30
+ def setUp(self):
31
+ self.client = Client()
32
+
33
+ def test_create_organization(self):
34
+ User.objects.create_user('kasper', 'kasper@McFadden.com', 'ghostpassword')
35
+ self.client.login(username='kasper', password='ghostpassword')
36
+
37
+ organization_form_data = {
38
+ 'name': 'Kasper Inc.',
39
+ 'number_scheme': 'S',
40
+ 'number_class_code_len': 3,
41
+ 'number_item_len': 4,
42
+ 'number_variation_len': 2,
43
+ }
44
+
45
+ response = self.client.post(reverse('bom:organization-create'), organization_form_data)
46
+ self.assertEqual(response.status_code, 302)
47
+
48
+ def test_create_organization_intelligent(self):
49
+ User.objects.create_user('kasper', 'kasper@McFadden.com', 'ghostpassword')
50
+ self.client.login(username='kasper', password='ghostpassword')
51
+
52
+ organization_form_data = {
53
+ 'name': 'Kasper Inc.',
54
+ 'number_scheme': 'I',
55
+ }
56
+
57
+ response = self.client.post(reverse('bom:organization-create'), organization_form_data)
58
+ self.assertEqual(response.status_code, 302)
59
+
60
+ def test_create_organization_intelligent_with_fields(self):
61
+ User.objects.create_user('kasper', 'kasper@McFadden.com', 'ghostpassword')
62
+ self.client.login(username='kasper', password='ghostpassword')
63
+
64
+ organization_form_data = {
65
+ 'name': 'Kasper Inc.',
66
+ 'number_scheme': 'I',
67
+ 'number_class_code_len': 3,
68
+ 'number_item_len': 4,
69
+ 'number_variation_len': 2,
70
+ }
71
+
72
+ response = self.client.post(reverse('bom:organization-create'), organization_form_data)
73
+ self.assertEqual(response.status_code, 302)
74
+
75
+ @override_settings(BOM_CONFIG=settings.BOM_CONFIG_DEFAULT)
76
+ class TestBOM(TransactionTestCase):
77
+ def setUp(self):
78
+ self.client = Client()
79
+ self.user, self.organization = create_user_and_organization()
80
+ self.profile = self.user.bom_profile(organization=self.organization)
81
+ self.profile.role = 'A'
82
+ self.profile.save()
83
+ self.client.login(username='kasper', password='ghostpassword')
84
+
85
+ def test_home(self):
86
+ response = self.client.post(reverse('bom:home'))
87
+ self.assertEqual(response.status_code, 200)
88
+
89
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
90
+
91
+ response = self.client.post(reverse('bom:home'))
92
+ self.assertEqual(response.status_code, 200)
93
+
94
+ # Make sure only one part shows up
95
+ decoded_content = response.content.decode('utf-8')
96
+ main_content = decoded_content[decoded_content.find('<main>')+len('<main>'):decoded_content.rfind('</main>')]
97
+ occurances = [m.start() for m in finditer(p1.full_part_number(), main_content)]
98
+ self.assertEqual(len(occurances), 1)
99
+
100
+ response = self.client.get(reverse('bom:home'), {'q': p1.primary_manufacturer_part.manufacturer_part_number})
101
+ self.assertEqual(response.status_code, 200)
102
+
103
+ # Test search
104
+ response = self.client.get(reverse('bom:home'), {'q': f'"{p1.full_part_number()}"'})
105
+ self.assertEqual(len(response.context['part_revs']), 1)
106
+
107
+ def test_part_info(self):
108
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
109
+
110
+ response = self.client.post(reverse('bom:part-info', kwargs={'part_id': p1.id}))
111
+ self.assertEqual(response.status_code, 200)
112
+
113
+ response = self.client.post(reverse('bom:part-info', kwargs={'part_id': p2.id}))
114
+ self.assertEqual(response.status_code, 200)
115
+
116
+ # test having no revisions
117
+ response = self.client.post(reverse('bom:part-info', kwargs={'part_id': p4.id}))
118
+ self.assertEqual(response.status_code, 200)
119
+
120
+ # set quantity
121
+ response = self.client.post(reverse('bom:part-info', kwargs={'part_id': p1.id}), {'quantity': 1000})
122
+ self.assertEqual(response.status_code, 200)
123
+
124
+ # test cache hit - TODO: probably want to make sure cache works
125
+ response = self.client.post(reverse('bom:part-info', kwargs={'part_id': p1.id}))
126
+ self.assertEqual(response.status_code, 200)
127
+
128
+ def test_part_manage_bom(self):
129
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
130
+
131
+ response = self.client.post(
132
+ reverse('bom:part-manage-bom', kwargs={'part_id': p1.id, 'part_revision_id': p1.latest().id, }))
133
+ self.assertEqual(response.status_code, 200)
134
+
135
+ response = self.client.post(
136
+ reverse('bom:part-manage-bom', kwargs={'part_id': p2.id, 'part_revision_id': p1.latest().id, }))
137
+ self.assertEqual(response.status_code, 200)
138
+
139
+ response = self.client.post(
140
+ reverse('bom:part-manage-bom', kwargs={'part_id': p3.id, 'part_revision_id': p3.latest().id, }))
141
+ self.assertEqual(response.status_code, 200)
142
+
143
+ def test_part_export_bom(self):
144
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
145
+
146
+ response = self.client.post(reverse('bom:part-export-bom', kwargs={'part_id': p1.id}))
147
+ self.assertEqual(response.status_code, 200)
148
+
149
+ response = self.client.post(reverse('bom:part-export-bom-sourcing', kwargs={'part_id': p1.id}))
150
+ self.assertEqual(response.status_code, 200)
151
+
152
+ response = self.client.post(reverse('bom:part-export-bom-sourcing-detailed', kwargs={'part_id': p1.id}))
153
+ self.assertEqual(response.status_code, 200)
154
+
155
+ response = self.client.post(reverse('bom:part-revision-export-bom-sourcing', kwargs={'part_revision_id': p3.latest().id}))
156
+ self.assertEqual(response.status_code, 200)
157
+
158
+ response = self.client.post(reverse('bom:part-revision-export-bom-sourcing-detailed', kwargs={'part_revision_id': p3.latest().id}))
159
+ self.assertEqual(response.status_code, 200)
160
+
161
+ def test_part_revision_export_bom(self):
162
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
163
+
164
+ response = self.client.post(reverse('bom:part-revision-export-bom', kwargs={'part_revision_id': p1.latest().id}))
165
+ self.assertEqual(response.status_code, 200)
166
+
167
+ def test_part_revision_export_bom_flat(self):
168
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
169
+
170
+ response = self.client.post(reverse('bom:part-revision-export-bom-flat', kwargs={'part_revision_id': p1.latest().id}))
171
+ self.assertEqual(response.status_code, 200)
172
+
173
+ response = self.client.post(reverse('bom:part-revision-export-bom-flat-sourcing', kwargs={'part_revision_id': p1.latest().id}))
174
+ self.assertEqual(response.status_code, 200)
175
+
176
+ response = self.client.post(reverse('bom:part-revision-export-bom-flat-sourcing-detailed', kwargs={'part_revision_id': p1.latest().id}))
177
+ self.assertEqual(response.status_code, 200)
178
+
179
+ def test_export_parts(self):
180
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
181
+
182
+ response = self.client.get(reverse('bom:home'), {'download': ''}, follow=True)
183
+ self.assertEqual(response.status_code, 200)
184
+
185
+ response = self.client.get(reverse('bom:home'), {'download': f'{p1.id}'}, follow=True)
186
+ self.assertEqual(response.status_code, 200)
187
+
188
+ def test_part_upload_bom(self):
189
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
190
+
191
+ test_file = 'test_bom.csv' if self.organization.number_variation_len > 0 else 'test_bom_6_no_variations.csv'
192
+ with open(f'{TEST_FILES_DIR}/{test_file}') as test_csv:
193
+ response = self.client.post(reverse('bom:part-upload-bom', kwargs={'part_id': p2.id}), {'file': test_csv}, follow=True)
194
+ self.assertEqual(response.status_code, 200)
195
+
196
+ messages = list(response.context.get('messages'))
197
+ for msg in messages:
198
+ self.assertNotEqual(msg.tags, "error")
199
+
200
+ subparts = p2.latest().assembly.subparts.all()
201
+
202
+ expected_pn = '200-3333-00' if self.organization.number_variation_len > 0 else '200-3333'
203
+ self.assertEqual(subparts[0].part_revision.part.full_part_number(), expected_pn)
204
+ self.assertEqual(subparts[0].count, 104) # append 4, 99, 1
205
+
206
+ expected_pn = '500-5555-00' if self.organization.number_variation_len > 0 else '500-5555'
207
+ self.assertEqual(subparts[1].part_revision.part.full_part_number(), expected_pn)
208
+ self.assertEqual(subparts[1].reference, 'U3, IC2, IC3')
209
+ self.assertEqual(subparts[1].count, 3)
210
+ self.assertEqual(subparts[1].do_not_load, False)
211
+
212
+ self.assertEqual(subparts[2].part_revision.part.full_part_number(), expected_pn)
213
+ self.assertEqual(subparts[2].reference, 'R1, R2')
214
+ self.assertEqual(subparts[2].count, 2)
215
+ self.assertEqual(subparts[2].do_not_load, True)
216
+
217
+ with open(f'{TEST_FILES_DIR}/test_bom_2.csv') as test_csv:
218
+ response = self.client.post(reverse('bom:part-upload-bom', kwargs={'part_id': p1.id}), {'file': test_csv}, follow=True)
219
+ self.assertEqual(response.status_code, 200)
220
+
221
+ messages = list(response.context.get('messages'))
222
+ for idx, msg in enumerate(messages):
223
+ self.assertTrue("Row 5 - manufacturer_part_number: Uploading of this subpart skipped. No part found for manufacturer part number." in str(msg.message))
224
+ self.assertTrue("Row 6 - manufacturer_part_number: Uploading of this subpart skipped. No part found for manufacturer part number." in str(msg.message))
225
+
226
+ p1.refresh_from_db()
227
+ bom = p1.latest().indented()
228
+ self.assertEqual(len(bom.parts), 3)
229
+
230
+ def test_upload_bom(self):
231
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
232
+
233
+ # Test OK page visit
234
+ response = self.client.get(reverse('bom:upload-bom'))
235
+ self.assertEqual(response.status_code, 200)
236
+
237
+ # Test OK upload
238
+ test_file = 'test_full_bom.csv' if self.organization.number_variation_len > 0 else 'test_full_bom_no_variations.csv'
239
+ with open(f'{TEST_FILES_DIR}/{test_file}') as test_csv:
240
+ response = self.client.post(reverse('bom:upload-bom'), {'file': test_csv}, follow=True)
241
+ self.assertEqual(response.status_code, 200)
242
+
243
+ with open(f'{TEST_FILES_DIR}/{test_file}') as test_csv:
244
+ reader = csv.DictReader(test_csv)
245
+ test_list = list(reader)
246
+
247
+ messages = list(response.context.get('messages'))
248
+ for msg in messages:
249
+ self.assertEqual(msg.tags, "info")
250
+ self.assertNotEqual(msg.tags, "error")
251
+
252
+ parent_part_number = '100-0001-02' if self.organization.number_variation_len > 0 else '100-0001'
253
+ parent_part = Part.from_part_number(parent_part_number, organization=self.organization)
254
+ bom = parent_part.indented()
255
+ bom_list = list(bom.parts.values())
256
+ self.assertEqual(len(bom.parts), len(test_list))
257
+
258
+ # Check that we successfully updated an existing part (only tested for semi-intelligent scheme for now)
259
+ if self.organization.number_scheme == constants.NUMBER_SCHEME_SEMI_INTELLIGENT:
260
+ p2.refresh_from_db()
261
+ p2_rev = p2.latest()
262
+ p2_mp = p2.primary_manufacturer_part
263
+ self.assertEqual(p2_rev.revision, '88') # previously 1
264
+ self.assertEqual(p2_rev.description, "123") # previously 'Brown dog'
265
+ self.assertEqual(p2_mp.manufacturer.name, "a new manufacturer name") # previously None
266
+ self.assertEqual(p2_mp.manufacturer_part_number, "a new mpn") # previously 'GRM1555C1H100JA01D'
267
+
268
+ # Check that parts get uploaded correctly
269
+ for idx, item in enumerate(test_list):
270
+ assertion_message = f'Index: {idx}, CSV PN: {item["part_number"]}, BOM PN: {bom_list[idx].part.full_part_number()}'
271
+ self.assertEqual(int(float(item['level'])), bom_list[idx].indent_level, assertion_message)
272
+ self.assertEqual(item['part_number'], bom_list[idx].part.full_part_number(), assertion_message)
273
+ self.assertEqual(item['revision'], bom_list[idx].part_revision.revision, assertion_message)
274
+ self.assertEqual(item['manufacturer_name'], bom_list[idx].part.primary_manufacturer_part.manufacturer.name, assertion_message)
275
+ self.assertEqual(item['manufacturer_part_number'], bom_list[idx].part.primary_manufacturer_part.manufacturer_part_number, assertion_message)
276
+ if bom_list[idx].indent_level > 0:
277
+ self.assertEqual(float(item['quantity']), bom_list[idx].subpart.count, assertion_message)
278
+
279
+ # Test OK upload with parent part number
280
+ test_file = 'test_full_bom.csv' if self.organization.number_variation_len > 0 else 'test_full_bom_no_variations.csv'
281
+ p4_rev = create_a_fake_part_revision(p4, create_a_fake_assembly())
282
+ with open(f'{TEST_FILES_DIR}/{test_file}') as test_csv:
283
+ response = self.client.post(reverse('bom:upload-bom'), {'file': test_csv, 'parent_part_number': p4.full_part_number()}, follow=True)
284
+ self.assertEqual(response.status_code, 200)
285
+
286
+ messages = list(response.context.get('messages'))
287
+ for msg in messages:
288
+ self.assertEqual(msg.tags, "info", msg.message)
289
+ self.assertNotEqual(msg.tags, "error", msg.message)
290
+
291
+ p4.refresh_from_db()
292
+ p4_rev.refresh_from_db()
293
+ self.assertEqual(len(p4_rev.indented().parts), 36)
294
+
295
+ # Test errors get thrown
296
+ test_file = 'test_full_bom_with_errors.csv' if self.organization.number_variation_len > 0 else 'test_full_bom_no_variations_with_errors.csv'
297
+ with open(f'{TEST_FILES_DIR}/{test_file}') as test_csv:
298
+ response = self.client.post(reverse('bom:upload-bom'), {'file': test_csv, 'parent_part_number': p3.full_part_number()}, follow=True)
299
+ self.assertEqual(response.status_code, 200)
300
+
301
+ messages = list(response.context.get('messages'))
302
+
303
+ for idx, msg in enumerate(messages):
304
+ if self.organization.number_scheme == constants.NUMBER_SCHEME_SEMI_INTELLIGENT:
305
+ self.assertTrue("Row 38 - part_number: Uploading of this subpart skipped. Couldn&#x27;t parse part number." in str(msg.message))
306
+ self.assertTrue("Row 34 - code: Ensure this value has at most 3 characters (it has 9)." in str(msg.message))
307
+ self.assertTrue("Row 33 - part_number: Uploading of this subpart skipped. Couldn&#x27;t parse part number." in str(msg.message))
308
+ self.assertTrue("Row 35 - part_number: Uploading of this subpart skipped. Couldn&#x27;t parse part number." in str(msg.message))
309
+ self.assertTrue("Row 36 - part_number: Uploading of this subpart skipped. Couldn&#x27;t parse part number." in str(msg.message))
310
+ self.assertTrue("Row 37 - part_number: Uploading of this subpart skipped. Couldn&#x27;t parse part number." in str(msg.message))
311
+ self.assertTrue("Row 39 - count: Ensure this value is greater than or equal to 0." in str(msg.message))
312
+ self.assertTrue("Row 40 - level: Assembly levels must decrease by no more than 1 from sequential rows." in str(msg.message))
313
+
314
+ # Check that 2 rows of 103-0002-00 in one assembly gets combined into one part, and added to the 2 that already exist = 2 + 1 + 1
315
+ parent_part_number = '107-0003-22' if self.organization.number_variation_len > 0 else '107-0003'
316
+ parent_part = Part.from_part_number(parent_part_number, organization=self.organization)
317
+ bom = parent_part.indented()
318
+ part_number_to_check = '103-0002-00' if self.organization.number_variation_len > 0 else '103-0002'
319
+ self.assertEqual(list(bom.parts.values())[6].part.full_part_number(), part_number_to_check)
320
+ self.assertEqual(list(bom.parts.values())[6].subpart.count, 4)
321
+
322
+ # Test infinite recursion error gets thrown
323
+ test_file = 'test_full_bom_with_errors_infinite_recursion.csv' if self.organization.number_variation_len > 0 else 'test_full_bom_no_variations_with_errors_infinite_recursion.csv'
324
+ with open(f'{TEST_FILES_DIR}/{test_file}') as test_csv:
325
+ response = self.client.post(reverse('bom:upload-bom'), {'file': test_csv, 'parent_part_number': p3.full_part_number()}, follow=True)
326
+ self.assertEqual(response.status_code, 200)
327
+
328
+ messages = list(response.context.get('messages'))
329
+ for idx, msg in enumerate(messages):
330
+ self.assertTrue("it would cause infinite recursion. Uploading of this subpart skipped." in str(msg.message))
331
+ self.assertTrue("Row 15" in str(msg.message))
332
+
333
+ def test_part_upload_bom_corner_cases(self):
334
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
335
+ with open(f'{TEST_FILES_DIR}/test_bom_3_recursion.csv') as test_csv:
336
+ response = self.client.post(reverse('bom:part-upload-bom', kwargs={'part_id': p1.id}), {'file': test_csv}, follow=True)
337
+ self.assertEqual(response.status_code, 200)
338
+
339
+ messages = list(response.context.get('messages'))
340
+ for msg in messages:
341
+ self.assertEqual(msg.tags, "error")
342
+ self.assertTrue("recursion" in str(msg.message))
343
+
344
+ with open(f'{TEST_FILES_DIR}/test_bom_4_no_part_rev.csv') as test_csv:
345
+ response = self.client.post(reverse('bom:part-upload-bom', kwargs={'part_id': p1.id}), {'file': test_csv}, follow=True)
346
+ self.assertEqual(response.status_code, 200)
347
+
348
+ messages = list(response.context.get('messages'))
349
+ for msg in messages:
350
+ self.assertNotEqual(msg.tags, "error") # Should be OK since we will default revision to 1
351
+
352
+ def test_export_part_list(self):
353
+ create_some_fake_parts(organization=self.organization)
354
+
355
+ response = self.client.post(reverse('bom:export-part-list'))
356
+ self.assertEqual(response.status_code, 200)
357
+
358
+ def test_create_edit_part_class(self):
359
+ part_class_code = 978
360
+ part_class_form_data = {
361
+ 'submit-part-class-create': '',
362
+ 'code': part_class_code,
363
+ 'name': 'test part name',
364
+ 'comment': 'this test part class description!'
365
+ }
366
+
367
+ response = self.client.post(reverse('bom:settings'), part_class_form_data)
368
+ self.assertEqual(response.status_code, 200)
369
+
370
+ part_classes = PartClass.objects.filter(code=part_class_code)
371
+ self.assertEqual(part_classes.count(), 1)
372
+ part_class = part_classes[0]
373
+
374
+ # Test edit
375
+ part_class_form_data['name'] = 'edited test part name'
376
+
377
+ response = self.client.post(reverse('bom:part-class-edit', kwargs={'part_class_id': part_class.id}), part_class_form_data)
378
+ self.assertEqual(response.status_code, 302)
379
+
380
+ part_class = PartClass.objects.get(id=part_class.id)
381
+ self.assertEqual(part_class.name, part_class_form_data['name'])
382
+
383
+ def test_create_part(self):
384
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
385
+
386
+ new_part_mpn = 'STM32F401-NEW-PART'
387
+ new_part_form_data = {
388
+ 'manufacturer_part_number': new_part_mpn,
389
+ 'manufacturer': p1.primary_manufacturer_part.manufacturer.id,
390
+ 'number_class': str(p1.number_class),
391
+ 'number_item': '',
392
+ 'number_variation': '',
393
+ 'configuration': 'W',
394
+ 'description': 'IC, MCU 32 Bit',
395
+ 'revision': 'A',
396
+ 'attribute': '',
397
+ 'value': ''
398
+ }
399
+
400
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
401
+ self.assertEqual(response.status_code, 302)
402
+ self.assertTrue('/part/' in response.url)
403
+
404
+ try:
405
+ created_part_id = response.url[6:-1]
406
+ created_part = Part.objects.get(id=created_part_id)
407
+ except IndexError:
408
+ self.assertFalse(True, "Part maybe not created? Url looks like: {}".format(response.url))
409
+
410
+ self.assertEqual(created_part.latest().description, 'IC, MCU 32 Bit')
411
+ self.assertEqual(created_part.manufacturer_parts().first().manufacturer_part_number, new_part_mpn)
412
+
413
+ new_part_form_data = {
414
+ 'manufacturer_part_number': 'STM32F401',
415
+ 'manufacturer': p1.primary_manufacturer_part.manufacturer.id,
416
+ 'number_class': str(p1.number_class),
417
+ 'number_item': '9999',
418
+ 'description': 'IC, MCU 32 Bit',
419
+ 'revision': 'A',
420
+ }
421
+
422
+ if self.organization.number_variation_len > 0:
423
+ new_part_form_data['number_variation'] = '01'
424
+
425
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
426
+ self.assertEqual(response.status_code, 302)
427
+ self.assertTrue('/part/' in response.url)
428
+
429
+ new_part_form_data = {
430
+ 'manufacturer_part_number': '',
431
+ 'manufacturer': '',
432
+ 'number_class': str(p1.number_class),
433
+ 'number_item': '',
434
+ 'number_variation': '',
435
+ 'description': 'IC, MCU 32 Bit',
436
+ 'revision': 'A',
437
+ }
438
+
439
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
440
+ self.assertEqual(response.status_code, 302)
441
+ self.assertTrue('/part/' in response.url)
442
+
443
+ new_part_form_data = {
444
+ 'manufacturer_part_number': '',
445
+ 'manufacturer': '',
446
+ 'number_class': str(p1.number_class),
447
+ 'number_item': '1234',
448
+ 'description': 'IC, MCU 32 Bit',
449
+ 'revision': 'A',
450
+ }
451
+
452
+ if self.organization.number_variation_len > 0:
453
+ new_part_form_data['number_variation'] = 'AZ'
454
+
455
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
456
+ self.assertEqual(response.status_code, 302)
457
+ self.assertTrue('/part/' in response.url)
458
+
459
+ new_part_form_data = {
460
+ 'manufacturer_part_number': '',
461
+ 'manufacturer': '',
462
+ 'number_class': str(p1.number_class),
463
+ 'number_item': '1235',
464
+ 'number_variation': '',
465
+ 'description': 'IC, MCU 32 Bit',
466
+ 'revision': 'A',
467
+ }
468
+
469
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
470
+ self.assertEqual(response.status_code, 302)
471
+ self.assertTrue('/part/' in response.url)
472
+
473
+ # fail nicely
474
+ new_part_form_data = {
475
+ 'manufacturer_part_number': 'ABC123',
476
+ 'manufacturer': '',
477
+ 'number_class': str(p1.number_class),
478
+ 'number_item': '',
479
+ 'number_variation': '',
480
+ 'description': 'IC, MCU 32 Bit',
481
+ 'revision': 'A',
482
+ }
483
+
484
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
485
+ self.assertEqual(response.status_code, 200)
486
+
487
+ # Make sure only one part shows up
488
+ response = self.client.get(reverse('bom:home'))
489
+ self.assertEqual(response.status_code, 200)
490
+ decoded_content = response.content.decode('utf-8')
491
+ main_content = decoded_content[decoded_content.find('<main>')+len('<main>'):decoded_content.rfind('</main>')]
492
+
493
+ occurances = [m.start() for m in finditer(p1.full_part_number(), main_content)]
494
+ self.assertEqual(len(occurances), 1)
495
+
496
+ def test_create_part_variation(self):
497
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
498
+
499
+ new_part_mpn = 'STM32F401-NEW-PART'
500
+ new_part_form_data = {
501
+ 'manufacturer_part_number': new_part_mpn,
502
+ 'manufacturer': p1.primary_manufacturer_part.manufacturer.id,
503
+ 'number_class': (p1.number_class),
504
+ 'number_item': '2000',
505
+ 'number_variation': '01',
506
+ 'configuration': 'W',
507
+ 'description': 'IC, MCU 32 Bit',
508
+ 'revision': 'A',
509
+ 'attribute': '',
510
+ 'value': ''
511
+ }
512
+
513
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
514
+ new_part_form_data['number_variation'] = '02'
515
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
516
+ # Part should be created because the variation is different, redirect means part was created
517
+ self.assertEqual(response.status_code, 302)
518
+ self.assertTrue('/part/' in response.url)
519
+
520
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
521
+ # Part should NOT be created because the variation is the same, 200 means error
522
+ self.assertEqual(response.status_code, 200)
523
+ self.assertTrue('error' in str(response.content))
524
+ self.assertTrue('already in use' in str(response.content))
525
+
526
+ def test_create_part_no_manufacturer_part(self):
527
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
528
+
529
+ new_part_mpn = 'STM32F401-NEW-PART'
530
+ new_part_form_data = {
531
+ 'manufacturer_part_number': '',
532
+ 'manufacturer': '',
533
+ 'number_class': str(p1.number_class),
534
+ 'number_item': '2000',
535
+ 'configuration': 'W',
536
+ 'description': 'IC, MCU 32 Bit',
537
+ 'revision': 'A',
538
+ 'attribute': '',
539
+ 'value': ''
540
+ }
541
+
542
+ number_variation = None
543
+ if self.organization.number_variation_len > 0:
544
+ number_variation = '01'
545
+ new_part_form_data['number_variation'] = number_variation
546
+
547
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
548
+ part = Part.objects.get(number_class=p1.number_class.id, number_item='2000', number_variation=number_variation)
549
+ self.assertEqual(len(part.manufacturer_parts()), 0)
550
+
551
+ def test_part_edit(self):
552
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
553
+
554
+ response = self.client.get(reverse('bom:part-edit', kwargs={'part_id': p1.id}))
555
+ self.assertEqual(response.status_code, 200)
556
+
557
+ edit_part_form_data = {
558
+ 'number_class': str(p1.number_class),
559
+ 'number_item': '',
560
+ 'number_variation': '',
561
+ }
562
+
563
+ response = self.client.post(reverse('bom:part-edit', kwargs={'part_id': p1.id}), edit_part_form_data)
564
+ self.assertEqual(response.status_code, 302)
565
+
566
+ def test_part_delete(self):
567
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
568
+ response = self.client.post(reverse('bom:part-delete', kwargs={'part_id': p1.id}))
569
+ self.assertEqual(response.status_code, 302)
570
+
571
+ def test_add_subpart(self):
572
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
573
+
574
+ # Submit with no form data
575
+ response = self.client.post(reverse('bom:part-add-subpart', kwargs={'part_id': p1.id, 'part_revision_id': p1.latest().id, }))
576
+ self.assertEqual(response.status_code, 302)
577
+
578
+ # Test adding two of the same subparts that also have assemblies. Make sure quantity gets incremented, and not 2 parts that are the same added
579
+ form_data = {'subpart_part_number': p2.full_part_number(), 'count': 3, 'reference': '', 'do_not_load': False}
580
+ response = self.client.post(reverse('bom:part-add-subpart', kwargs={'part_id': p3.id, 'part_revision_id': p3.latest().id, }), form_data)
581
+ self.assertEqual(response.status_code, 302)
582
+
583
+ # Below - make sure quantity gets incremented, not that there are > 1 parts
584
+ repeat_part_revision = p2.latest()
585
+ parts_p2 = 0
586
+ qty_p2 = 0
587
+ indented_bom = p3.latest().indented()
588
+ for _, p in indented_bom.parts.items():
589
+ if p.part_revision == repeat_part_revision:
590
+ parts_p2 += 1
591
+ qty_p2 = p.quantity
592
+ self.assertEqual(1, parts_p2)
593
+ self.assertEqual(7, qty_p2)
594
+
595
+ # Test adding a third, but make it DNL
596
+ form_data = {'subpart_part_number': p2.full_part_number(), 'count': 3, 'reference': '', 'do_not_load': True}
597
+ response = self.client.post(reverse('bom:part-add-subpart', kwargs={'part_id': p3.id, 'part_revision_id': p3.latest().id, }), form_data)
598
+ self.assertEqual(response.status_code, 302)
599
+
600
+ # Below - make sure quantity gets incremented, not that there are > 1 parts
601
+ repeat_part_revision = p2.latest()
602
+ parts_p2 = 0
603
+ qty_p2_load = 0
604
+ qty_p2_do_not_load = 0
605
+ indented_bom = p3.latest().indented()
606
+ for _, p in indented_bom.parts.items():
607
+ if p.part_revision == repeat_part_revision:
608
+ parts_p2 += 1
609
+ if p.part_revision == repeat_part_revision and p.do_not_load:
610
+ qty_p2_do_not_load += p.quantity
611
+ elif p.part_revision == repeat_part_revision:
612
+ qty_p2_load += p.quantity
613
+
614
+ self.assertEqual(2, parts_p2)
615
+ self.assertEqual(3, qty_p2_do_not_load)
616
+ self.assertEqual(7, qty_p2_load)
617
+
618
+ def test_add_subpart_infinite_recursion(self):
619
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
620
+
621
+ # Test preventing infinite recursion
622
+ form_data = {'subpart_part_number': p3.full_part_number(), 'count': 3, 'reference': '', 'do_not_load': False}
623
+ response = self.client.post(reverse('bom:part-add-subpart', kwargs={'part_id': p3.id, 'part_revision_id': p3.latest().id, }), form_data)
624
+ self.assertEqual(response.status_code, 302)
625
+ found_error = False
626
+ rejected_add = False
627
+ for m in response.wsgi_request._messages:
628
+ if 'Added' in str(m):
629
+ found_error = True
630
+ if "Infinite recursion!" in str(m):
631
+ rejected_add = True
632
+ self.assertFalse(found_error)
633
+ self.assertTrue(rejected_add)
634
+
635
+ # Test preventing infinite recursion - Check that a subpart doesnt exist in a parent's parent assy / deep recursion
636
+ # p3 has p2 in its assy, dont let p2 add p3 to it
637
+ form_data = {'subpart_part_number': p3.full_part_number(), 'count': 3, 'reference': '', 'do_not_load': False}
638
+ response = self.client.post(reverse('bom:part-add-subpart', kwargs={'part_id': p2.id, 'part_revision_id': p2.latest().id, }), form_data)
639
+ self.assertEqual(response.status_code, 302)
640
+ found_error = False
641
+ rejected_add = False
642
+ for m in response.wsgi_request._messages:
643
+ if 'Added' in str(m):
644
+ found_error = True
645
+ if "Infinite recursion!" in str(m):
646
+ rejected_add = True
647
+ self.assertFalse(found_error)
648
+ self.assertTrue(rejected_add)
649
+
650
+ def test_remove_subpart(self):
651
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
652
+ s1 = create_a_fake_subpart(p1.latest(), count=10)
653
+
654
+ response = self.client.post(
655
+ reverse('bom:part-remove-subpart',
656
+ kwargs={'part_id': p1.id, 'subpart_id': s1.id, 'part_revision_id': p1.latest().id, }))
657
+ self.assertEqual(response.status_code, 302)
658
+
659
+ def test_remove_all_subparts(self):
660
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
661
+
662
+ part = p3
663
+ part_revision = part.latest()
664
+
665
+ subparts = part_revision.assembly.subparts.all()
666
+ subpart_ids = list(subparts.values_list('id', flat=True))
667
+
668
+ response = self.client.post(
669
+ reverse('bom:part-remove-all-subparts', kwargs={'part_id': part.id, 'part_revision_id': part_revision.id}))
670
+ self.assertEqual(response.status_code, 302)
671
+ self.assertEqual(0, len(part_revision.assembly.subparts.all()))
672
+
673
+ subparts = Subpart.objects.filter(id__in=subpart_ids)
674
+ self.assertEqual(0, len(subparts))
675
+
676
+ def test_upload_parts(self):
677
+ create_some_fake_part_classes(self.organization)
678
+
679
+ # Should pass
680
+ with open(f'{TEST_FILES_DIR}/test_new_parts.csv') as test_csv:
681
+ response = self.client.post(reverse('bom:upload-parts'), {'file': test_csv}, follow=True)
682
+ messages = list(response.context.get('messages'))
683
+ for msg in messages:
684
+ self.assertEqual(msg.tags, 'info')
685
+ new_part_count = Part.objects.all().count()
686
+ self.assertEqual(new_part_count, 4)
687
+
688
+ # Part revs should be created for each part
689
+ for p in Part.objects.all():
690
+ self.assertIsNotNone(p.latest())
691
+
692
+ # Should fail because class doesn't exist
693
+ with open(f'{TEST_FILES_DIR}/test_new_parts_2.csv') as test_csv:
694
+ response = self.client.post(reverse('bom:upload-parts'), {'file': test_csv})
695
+ self.assertEqual(response.status_code, 302)
696
+ found_error = False
697
+ for m in response.wsgi_request._messages:
698
+ if "Part class 216 in row 2" in str(m) and "Uploading of this part skipped." in str(m):
699
+ found_error = True
700
+ self.assertTrue(found_error)
701
+
702
+ # Part should be skipped because it already exists
703
+ with open(f'{TEST_FILES_DIR}/test_new_parts_3.csv') as test_csv:
704
+ response = self.client.post(reverse('bom:upload-parts'), {'file': test_csv})
705
+ self.assertEqual(response.status_code, 302)
706
+ found_error = False
707
+ for m in response.wsgi_request._messages:
708
+ if "Part already exists for manufacturer part 2 in row GhostBuster2000. Uploading of this part skipped." in str(m):
709
+ found_error = True
710
+ self.assertTrue(found_error)
711
+
712
+ def test_upload_parts_break_tolerance(self):
713
+ create_some_fake_part_classes(self.organization)
714
+
715
+ # Should break with data error
716
+ with open(f'{TEST_FILES_DIR}/test_new_parts_broken.csv') as test_csv:
717
+ response = self.client.post(reverse('bom:upload-parts'), {'file': test_csv}, follow=True)
718
+ messages = list(response.context.get('messages'))
719
+
720
+ self.assertTrue(len(messages) > 0)
721
+ for msg in messages:
722
+ self.assertEqual(msg.tags, 'error')
723
+
724
+ def test_upload_part_with_sellers(self):
725
+ create_some_fake_part_classes(self.organization)
726
+ # Should pass
727
+ initial_parts_count = Part.objects.all().count()
728
+ with open('bom/test_files/test_new_parts_sellers.csv') as test_csv:
729
+ response = self.client.post(reverse('bom:upload-parts'), {'file': test_csv})
730
+ self.assertEqual(response.status_code, 302)
731
+
732
+ parts_count = Part.objects.all().count()
733
+ self.assertEqual(parts_count - initial_parts_count, 4)
734
+
735
+ def test_upload_part_classes(self):
736
+ # Should pass
737
+ with open(f'{TEST_FILES_DIR}/test_part_classes.csv') as test_csv:
738
+ response = self.client.post(reverse('bom:settings'), {'file': test_csv, 'submit-part-class-upload': ''})
739
+ self.assertEqual(response.status_code, 200)
740
+
741
+ new_part_class_count = PartClass.objects.all().count()
742
+ self.assertEqual(new_part_class_count, 37)
743
+
744
+ # Should not hit 500 errors on anything below
745
+ # Submit with no file
746
+ response = self.client.post(reverse('bom:settings'), {'submit-part-class-upload': ''})
747
+ self.assertEqual(response.status_code, 200)
748
+
749
+ # Submit with blank header and comments
750
+ with open(f'{TEST_FILES_DIR}/test_part_classes_no_comment.csv') as test_csv:
751
+ response = self.client.post(reverse('bom:settings'), {'file': test_csv, 'submit-part-class-upload': ''})
752
+ self.assertEqual(response.status_code, 200)
753
+ self.assertTrue('Part class 102 Resistor on row 3 is already defined. Uploading of this part class skipped.' in str(response.content))
754
+
755
+ # Submit with a weird csv file that sort of works
756
+ with open(f'{TEST_FILES_DIR}/test_part_classes_blank_rows.csv') as test_csv:
757
+ response = self.client.post(reverse('bom:settings'), {'file': test_csv, 'submit-part-class-upload': ''})
758
+ self.assertEqual(response.status_code, 200)
759
+ self.assertTrue('in row 3 does not have a value. Uploading of this part class skipped.' in str(response.content))
760
+ self.assertTrue('in row 4 does not have a value. Uploading of this part class skipped.' in str(response.content))
761
+
762
+ # Submit with a csv file exported with a byte order mask, typically from MS word I think
763
+ with open(f'{TEST_FILES_DIR}/test_part_classes_byte_order.csv') as test_csv:
764
+ response = self.client.post(reverse('bom:settings'), {'file': test_csv, 'submit-part-class-upload': ''}, follow=True)
765
+ self.assertEqual(response.status_code, 200)
766
+ messages = list(response.context.get('messages'))
767
+ for msg in messages:
768
+ self.assertTrue('None on row' not in str(msg.message))
769
+
770
+ def test_upload_part_classes_sample(self):
771
+ # Should pass
772
+ with open(f'{TEST_FILES_DIR}/sample_part_classes.csv') as test_csv:
773
+ response = self.client.post(reverse('bom:settings'), {'file': test_csv, 'submit-part-class-upload': ''})
774
+ self.assertEqual(response.status_code, 200)
775
+
776
+ new_part_class_count = PartClass.objects.all().count()
777
+ self.assertEqual(new_part_class_count, 37)
778
+
779
+ def test_upload_part_classes_parts_and_boms(self):
780
+ self.organization.number_item_len = 5
781
+ self.organization.save()
782
+
783
+ # Upload part classes
784
+ with open(f'{TEST_FILES_DIR}/test_part_classes_4.csv') as test_csv:
785
+ response = self.client.post(reverse('bom:settings'), {'file': test_csv, 'submit-part-class-upload': ''})
786
+ self.assertEqual(response.status_code, 200)
787
+
788
+ new_part_class_count = PartClass.objects.all().count()
789
+ self.assertEqual(new_part_class_count, 39)
790
+
791
+ # Upload parts
792
+ with open(f'{TEST_FILES_DIR}/test_new_parts_4.csv') as test_csv:
793
+ response = self.client.post(reverse('bom:upload-parts'), {'file': test_csv}, follow=True)
794
+ messages = list(response.context.get('messages'))
795
+ for msg in messages:
796
+ self.assertEqual(msg.tags, 'info')
797
+
798
+ self.assertEqual(response.status_code, 200)
799
+ new_part_count = Part.objects.all().count()
800
+ self.assertEqual(new_part_count, 88)
801
+ for p in Part.objects.all():
802
+ self.assertIsNotNone(p.latest())
803
+
804
+ pcba_class = PartClass.objects.filter(code=652).first()
805
+ pcba = Part.objects.filter(number_class=pcba_class, number_item='00003', number_variation='0A').first()
806
+
807
+ with open(f'{TEST_FILES_DIR}/test_bom_652-00003-0A.csv') as test_csv:
808
+ response = self.client.post(reverse('bom:part-upload-bom', kwargs={'part_id': pcba.id}), {'file': test_csv}, follow=True)
809
+ self.assertEqual(response.status_code, 200)
810
+
811
+ messages = list(response.context.get('messages'))
812
+
813
+ for msg in messages:
814
+ self.assertNotEqual(msg.tags, "error")
815
+ self.assertEqual(msg.tags, "info")
816
+
817
+ subparts = pcba.latest().assembly.subparts.all().order_by('id')
818
+ self.assertEqual(subparts[0].reference, 'C1')
819
+ self.assertEqual(subparts[1].reference, 'C2, C21')
820
+ self.assertEqual(subparts[2].reference, 'C23')
821
+ pcba = Part.objects.filter(number_class=pcba_class, number_item='00004', number_variation='0A').first()
822
+
823
+ with open(f'{TEST_FILES_DIR}/test_bom_652-00004-0A.csv') as test_csv:
824
+ response = self.client.post(reverse('bom:part-upload-bom', kwargs={'part_id': pcba.id}), {'file': test_csv}, follow=True)
825
+ self.assertEqual(response.status_code, 200)
826
+
827
+ messages = list(response.context.get('messages'))
828
+ for idx, msg in enumerate(messages):
829
+ self.assertNotEqual(msg.tags, "error")
830
+ self.assertEqual(msg.tags, "info")
831
+
832
+ # Check that that rows that have a part number already used but which denote a distinct designator are
833
+ # consolidated into one subpart with one part number but multiple designators and matching quantity counts.
834
+ subparts = pcba.latest().assembly.subparts.all().order_by('id')
835
+ self.assertEqual(subparts[0].reference, 'C1, C2')
836
+ self.assertEqual(subparts[0].count, 2)
837
+ self.assertEqual(subparts[1].reference, 'C3, C4, C5, C6, C11')
838
+ self.assertEqual(subparts[1].count, 5)
839
+ self.assertEqual(subparts[2].reference, 'C7, C8, C9, C10, C14, C18, C22, C33')
840
+ self.assertEqual(subparts[2].count, 8)
841
+ self.assertEqual(subparts[16].reference, 'Y1')
842
+ self.assertEqual(subparts[16].count, 1)
843
+
844
+ def test_edit_user_meta(self):
845
+ response = self.client.post(reverse('bom:user-meta-edit', kwargs={'user_meta_id': self.user.bom_profile().id}))
846
+ self.assertEqual(response.status_code, 200)
847
+
848
+ def test_add_sellerpart(self):
849
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
850
+
851
+ response = self.client.get(reverse('bom:manufacturer-part-add-sellerpart', kwargs={'manufacturer_part_id': p1.primary_manufacturer_part.id}))
852
+ self.assertEqual(response.status_code, 200)
853
+
854
+ response = self.client.post(reverse('bom:manufacturer-part-add-sellerpart', kwargs={'manufacturer_part_id': p1.primary_manufacturer_part.id}))
855
+ self.assertEqual(response.status_code, 200)
856
+
857
+ new_sellerpart_form_data = {
858
+ 'seller': p1.optimal_seller().seller.id,
859
+ 'seller_part_number': p1.optimal_seller().seller_part_number,
860
+ 'minimum_order_quantity': 1000,
861
+ 'minimum_pack_quantity': 500,
862
+ 'unit_cost': '1.23',
863
+ 'lead_time_days': 25,
864
+ 'nre_cost': 2000,
865
+ 'ncnr': False,
866
+ }
867
+
868
+ response = self.client.post(reverse('bom:manufacturer-part-add-sellerpart',
869
+ kwargs={'manufacturer_part_id': p1.primary_manufacturer_part.id}),
870
+ new_sellerpart_form_data)
871
+ self.assertEqual(response.status_code, 302)
872
+ self.assertTrue('/part/' in response.url)
873
+
874
+ def test_sellerpart_edit(self):
875
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
876
+
877
+ edit_sellerpart_form_data = {
878
+ 'new_seller': 'indabom',
879
+ 'seller_part_number': '123-45678',
880
+ 'minimum_order_quantity': 100,
881
+ 'minimum_pack_quantity': 200,
882
+ 'unit_cost': '1.2',
883
+ 'lead_time_days': 5,
884
+ 'nre_cost': 1000,
885
+ 'ncnr': True,
886
+ }
887
+
888
+ response = self.client.post(reverse('bom:sellerpart-edit', kwargs={'sellerpart_id': p1.optimal_seller().id}), edit_sellerpart_form_data)
889
+ self.assertEqual(response.status_code, 302)
890
+
891
+ def test_sellerpart_delete(self):
892
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
893
+ response = self.client.post(reverse('bom:sellerpart-delete', kwargs={'sellerpart_id': p1.optimal_seller().id}))
894
+
895
+ self.assertEqual(response.status_code, 302)
896
+
897
+ def test_add_manufacturer_part(self):
898
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
899
+ # Test GET
900
+ response = self.client.get(reverse('bom:part-add-manufacturer-part', kwargs={'part_id': p1.id}))
901
+
902
+ # Test POSTs
903
+ mfg_form_data = {'name': p1.primary_manufacturer_part.manufacturer.name,
904
+ 'manufacturer_part_number': p1.primary_manufacturer_part.manufacturer_part_number,
905
+ 'part': p2.id}
906
+ response = self.client.post(reverse('bom:part-add-manufacturer-part', kwargs={'part_id': p1.id}), mfg_form_data)
907
+ self.assertEqual(response.status_code, 302)
908
+
909
+ mfg_form_data = {'name': "A new mfg name",
910
+ 'manufacturer_part_number': "a new pn",
911
+ 'part': p2.id}
912
+ response = self.client.post(reverse('bom:part-add-manufacturer-part', kwargs={'part_id': p1.id}), mfg_form_data)
913
+ self.assertEqual(response.status_code, 302)
914
+
915
+ def test_manufacturers(self):
916
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
917
+ response = self.client.post(reverse('bom:manufacturers'))
918
+ self.assertEqual(response.status_code, 200)
919
+
920
+ def test_manufacturer_info(self):
921
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
922
+ response = self.client.post(reverse('bom:manufacturer-info', kwargs={'manufacturer_id': p1.primary_manufacturer_part.manufacturer.id}))
923
+ self.assertEqual(response.status_code, 200)
924
+
925
+ def test_manufacturer_edit(self):
926
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
927
+ response = self.client.post(reverse('bom:manufacturer-edit', kwargs={'manufacturer_id': p1.primary_manufacturer_part.manufacturer.id}))
928
+ self.assertEqual(response.status_code, 302)
929
+
930
+ def test_manufacturer_delete(self):
931
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
932
+ response = self.client.post(reverse('bom:manufacturer-delete', kwargs={'manufacturer_id': p1.primary_manufacturer_part.manufacturer.id}))
933
+ self.assertEqual(response.status_code, 302)
934
+
935
+ def test_sellers(self):
936
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
937
+ response = self.client.post(reverse('bom:sellers'))
938
+ self.assertEqual(response.status_code, 200)
939
+
940
+ def test_seller_info(self):
941
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
942
+ response = self.client.post(reverse('bom:seller-info', kwargs={'seller_id': p2.primary_manufacturer_part.optimal_seller().seller_id}))
943
+ self.assertEqual(response.status_code, 200)
944
+
945
+ def test_seller_edit(self):
946
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
947
+ response = self.client.post(reverse('bom:seller-edit', kwargs={'seller_id': p2.primary_manufacturer_part.optimal_seller().seller_id}), {'name': 'Mousah'})
948
+ self.assertEqual(response.status_code, 302)
949
+
950
+ def test_seller_delete(self):
951
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
952
+ response = self.client.post(reverse('bom:seller-delete', kwargs={'seller_id': p2.primary_manufacturer_part.optimal_seller().seller_id}))
953
+ self.assertEqual(response.status_code, 302)
954
+
955
+ def test_manufacturer_part_edit(self):
956
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
957
+ response = self.client.post(
958
+ reverse('bom:manufacturer-part-edit', kwargs={'manufacturer_part_id': p1.primary_manufacturer_part.id}))
959
+ self.assertEqual(response.status_code, 200)
960
+
961
+ data = {
962
+ 'manufacturer_part_number': 'ABC123',
963
+ 'manufacturer': p1.primary_manufacturer_part.manufacturer.id,
964
+ 'name': '',
965
+ }
966
+
967
+ response = self.client.post(reverse('bom:manufacturer-part-edit', kwargs={'manufacturer_part_id': p1.primary_manufacturer_part.id}), data)
968
+ self.assertEqual(response.status_code, 302)
969
+
970
+ data = {
971
+ 'manufacturer_part_number': 'ABC123',
972
+ 'manufacturer': p1.primary_manufacturer_part.manufacturer.id,
973
+ 'name': 'A new manufacturer',
974
+ }
975
+
976
+ old_id = p1.primary_manufacturer_part.manufacturer.id
977
+ response = self.client.post(reverse('bom:manufacturer-part-edit', kwargs={'manufacturer_part_id': p1.primary_manufacturer_part.id}), data)
978
+ self.assertEqual(response.status_code, 302)
979
+ p1.refresh_from_db()
980
+ self.assertNotEqual(p1.primary_manufacturer_part.manufacturer.id, old_id)
981
+
982
+ data = {
983
+ 'manufacturer_part_number': 'ABC123',
984
+ 'manufacturer': '',
985
+ 'name': '',
986
+ }
987
+
988
+ response = self.client.post(
989
+ reverse('bom:manufacturer-part-edit', kwargs={'manufacturer_part_id': p1.primary_manufacturer_part.id}),
990
+ data)
991
+ self.assertEqual(response.status_code, 200) # 200 means it failed validation
992
+
993
+ def test_manufacturer_part_delete(self):
994
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
995
+ response = self.client.post(
996
+ reverse('bom:manufacturer-part-delete', kwargs={'manufacturer_part_id': p1.primary_manufacturer_part.id}))
997
+
998
+ self.assertEqual(response.status_code, 302)
999
+
1000
+ def test_part_revision_release(self):
1001
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
1002
+
1003
+ response = self.client.get(
1004
+ reverse('bom:part-revision-release', kwargs={'part_id': p1.id, 'part_revision_id': p1.latest().id}))
1005
+ self.assertEqual(response.status_code, 200)
1006
+
1007
+ response = self.client.post(
1008
+ reverse('bom:part-revision-release', kwargs={'part_id': p1.id, 'part_revision_id': p1.latest().id}))
1009
+
1010
+ self.assertEqual(response.status_code, 302)
1011
+
1012
+ def test_part_revision_revert(self):
1013
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
1014
+ response = self.client.get(
1015
+ reverse('bom:part-revision-revert', kwargs={'part_id': p1.id, 'part_revision_id': p1.latest().id}))
1016
+
1017
+ self.assertEqual(response.status_code, 302)
1018
+
1019
+ def test_part_revision_new(self):
1020
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
1021
+
1022
+ response = self.client.get(reverse('bom:part-revision-new', kwargs={'part_id': p1.id}))
1023
+ self.assertEqual(response.status_code, 200)
1024
+
1025
+ # Create new part revision from part without an existing part revision
1026
+ response = self.client.get(reverse('bom:part-revision-new', kwargs={'part_id': p4.id}))
1027
+ self.assertEqual(response.status_code, 200)
1028
+
1029
+ new_part_revision_form_data = {
1030
+ 'description': 'new rev',
1031
+ 'revision': '4',
1032
+ 'attribute': 'resistance',
1033
+ 'value': '10k',
1034
+ 'part': p1.id,
1035
+ 'configuration': 'W',
1036
+ 'copy_assembly': 'False'
1037
+ }
1038
+
1039
+ response = self.client.post(
1040
+ reverse('bom:part-revision-new', kwargs={'part_id': p1.id}), new_part_revision_form_data)
1041
+
1042
+ self.assertEqual(response.status_code, 302)
1043
+
1044
+ # Create new part revision, copy over the assembly, increment revision, then make sure the old revision
1045
+ # didn't change
1046
+ new_part_revision_form_data = {
1047
+ 'description': 'new rev',
1048
+ 'revision': '5',
1049
+ 'part': p3.id,
1050
+ 'configuration': 'W',
1051
+ 'copy_assembly': 'true'
1052
+ }
1053
+
1054
+ response = self.client.post(
1055
+ reverse('bom:part-revision-new', kwargs={'part_id': p3.id}), new_part_revision_form_data)
1056
+
1057
+ revs = p3.revisions().order_by('-id')
1058
+ latest = revs[0]
1059
+ previous = revs[1]
1060
+ previous_subpart_ids = previous.assembly.subparts.all().values_list('id', flat=True)
1061
+ new_subpart_ids = latest.assembly.subparts.all().values_list('id', flat=True)
1062
+
1063
+ self.assertEqual(response.status_code, 302)
1064
+ self.assertNotEqual([], new_subpart_ids)
1065
+ for nsid in new_subpart_ids:
1066
+ self.assertNotIn(nsid, previous_subpart_ids)
1067
+
1068
+ def test_part_revision_edit(self):
1069
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
1070
+ response = self.client.get(
1071
+ reverse('bom:part-revision-edit', kwargs={'part_id': p1.id, 'part_revision_id': p1.latest().id}))
1072
+
1073
+ self.assertEqual(response.status_code, 200)
1074
+
1075
+ edit_part_revision_form_data = {
1076
+ 'description': 'new rev',
1077
+ 'revision': '4',
1078
+ 'attribute': 'resistance',
1079
+ 'value': '10k',
1080
+ 'part': p1.id
1081
+ }
1082
+
1083
+ response = self.client.post(
1084
+ reverse('bom:part-revision-edit', kwargs={'part_id': p1.id, 'part_revision_id': p1.latest().id}),
1085
+ edit_part_revision_form_data)
1086
+
1087
+ self.assertEqual(response.status_code, 302)
1088
+
1089
+ def test_part_revision_delete(self):
1090
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
1091
+ response = self.client.post(
1092
+ reverse('bom:part-revision-delete', kwargs={'part_id': p1.id, 'part_revision_id': p1.latest().id}))
1093
+
1094
+ self.assertEqual(response.status_code, 302)
1095
+
1096
+ @override_settings(BOM_CONFIG=settings.BOM_CONFIG_DEFAULT)
1097
+ class TestBOMIntelligent(TestBOM):
1098
+ def setUp(self):
1099
+ self.client = Client()
1100
+ self.user, self.organization = create_user_and_organization()
1101
+ self.profile = self.user.bom_profile(organization=self.organization)
1102
+ self.organization.number_scheme = constants.NUMBER_SCHEME_INTELLIGENT
1103
+ self.organization.save()
1104
+ self.client.login(username='kasper', password='ghostpassword')
1105
+
1106
+ def test_create_part(self):
1107
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
1108
+
1109
+ new_part_mpn = 'STM32F401-NEW-PART'
1110
+ new_part_form_data = {
1111
+ 'manufacturer_part_number': new_part_mpn,
1112
+ 'manufacturer': p1.primary_manufacturer_part.manufacturer.id,
1113
+ 'number_item': 'ABC1',
1114
+ 'configuration': 'W',
1115
+ 'description': 'IC, MCU 32 Bit',
1116
+ 'revision': 'A',
1117
+ 'attribute': '',
1118
+ 'value': ''
1119
+ }
1120
+
1121
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
1122
+ self.assertEqual(response.status_code, 302)
1123
+ self.assertTrue('/part/' in response.url)
1124
+
1125
+ try:
1126
+ created_part_id = response.url[6:-1]
1127
+ created_part = Part.objects.get(id=created_part_id)
1128
+ except IndexError:
1129
+ self.assertFalse(True, "Part maybe not created? Url looks like: {}".format(response.url))
1130
+
1131
+ self.assertEqual(created_part.latest().description, 'IC, MCU 32 Bit')
1132
+ self.assertEqual(created_part.manufacturer_parts().first().manufacturer_part_number, new_part_mpn)
1133
+
1134
+ new_part_form_data = {
1135
+ 'manufacturer_part_number': 'STM32F401',
1136
+ 'manufacturer': p1.primary_manufacturer_part.manufacturer.id,
1137
+ 'number_item': '9999',
1138
+ 'description': 'IC, MCU 32 Bit',
1139
+ 'revision': 'A',
1140
+ }
1141
+
1142
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
1143
+ self.assertEqual(response.status_code, 302)
1144
+ self.assertTrue('/part/' in response.url)
1145
+
1146
+ new_part_form_data = {
1147
+ 'manufacturer_part_number': '',
1148
+ 'manufacturer': '',
1149
+ 'number_item': '5432',
1150
+ 'description': 'IC, MCU 32 Bit',
1151
+ 'revision': 'A',
1152
+ }
1153
+
1154
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
1155
+ self.assertEqual(response.status_code, 302)
1156
+ self.assertTrue('/part/' in response.url)
1157
+
1158
+ new_part_form_data = {
1159
+ 'manufacturer_part_number': '',
1160
+ 'manufacturer': '',
1161
+ 'number_item': '1234A',
1162
+ 'description': 'IC, MCU 32 Bit',
1163
+ 'revision': 'A',
1164
+ }
1165
+
1166
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
1167
+ self.assertEqual(response.status_code, 302)
1168
+ self.assertTrue('/part/' in response.url)
1169
+
1170
+ new_part_form_data = {
1171
+ 'manufacturer_part_number': '',
1172
+ 'manufacturer': '',
1173
+ 'number_item': '1235',
1174
+ 'description': 'IC, MCU 32 Bit',
1175
+ 'revision': 'A',
1176
+ }
1177
+
1178
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
1179
+ self.assertEqual(response.status_code, 302)
1180
+ self.assertTrue('/part/' in response.url)
1181
+
1182
+ # fail nicely
1183
+ new_part_form_data = {
1184
+ 'manufacturer_part_number': 'ABC123',
1185
+ 'manufacturer': '',
1186
+ 'number_item': p1.number_item,
1187
+ 'description': 'IC, MCU 32 Bit',
1188
+ 'revision': 'A',
1189
+ }
1190
+
1191
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
1192
+ self.assertEqual(response.status_code, 200)
1193
+
1194
+ # Make sure only one part shows up
1195
+ response = self.client.post(reverse('bom:home'))
1196
+ self.assertEqual(response.status_code, 200)
1197
+ decoded_content = response.content.decode('utf-8')
1198
+ main_content = decoded_content[decoded_content.find('<main>')+len('<main>'):decoded_content.rfind('</main>')]
1199
+ occurances = [m.start() for m in finditer(p1.full_part_number(), main_content)]
1200
+ self.assertEqual(len(occurances), 1)
1201
+
1202
+ @skip('Not applicable')
1203
+ def test_create_part_variation(self):
1204
+ pass
1205
+
1206
+ def test_create_part_no_manufacturer_part(self):
1207
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
1208
+
1209
+ new_part_mpn = 'STM32F401-NEW-PART'
1210
+ new_part_form_data = {
1211
+ 'manufacturer_part_number': '',
1212
+ 'manufacturer': '',
1213
+ 'number_item': '2000',
1214
+ 'configuration': 'W',
1215
+ 'description': 'IC, MCU 32 Bit',
1216
+ 'revision': 'A',
1217
+ 'attribute': '',
1218
+ 'value': ''
1219
+ }
1220
+
1221
+ response = self.client.post(reverse('bom:create-part'), new_part_form_data)
1222
+ part = Part.objects.get(number_item='2000')
1223
+ self.assertEqual(len(part.manufacturer_parts()), 0)
1224
+
1225
+ def test_part_edit(self):
1226
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
1227
+
1228
+ response = self.client.get(reverse('bom:part-edit', kwargs={'part_id': p1.id}))
1229
+ self.assertEqual(response.status_code, 200)
1230
+
1231
+ edit_part_form_data = {
1232
+ 'number_item': 'HEYA',
1233
+ }
1234
+
1235
+ response = self.client.post(reverse('bom:part-edit', kwargs={'part_id': p1.id}), edit_part_form_data)
1236
+ self.assertEqual(response.status_code, 302)
1237
+
1238
+ def test_part_upload_bom(self):
1239
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
1240
+
1241
+ p5, _ = Part.objects.get_or_create(number_item='500-5555-00', organization=self.organization)
1242
+ assy = create_a_fake_assembly()
1243
+ pr5 = create_a_fake_part_revision(part=p5, assembly=assy)
1244
+
1245
+ p6, _ = Part.objects.get_or_create(number_item='200-3333-00', organization=self.organization)
1246
+ assy = create_a_fake_assembly()
1247
+ pr6 = create_a_fake_part_revision(part=p5, assembly=assy)
1248
+
1249
+ with open(f'{TEST_FILES_DIR}/test_bom.csv') as test_csv:
1250
+ response = self.client.post(reverse('bom:part-upload-bom', kwargs={'part_id': p2.id}), {'file': test_csv}, follow=True)
1251
+ self.assertEqual(response.status_code, 200)
1252
+
1253
+ messages = list(response.context.get('messages'))
1254
+ for msg in messages:
1255
+ self.assertNotEqual(msg.tags, "error") # Error loading 200-3333-00 via CSV because already in parent's BOM and has empty ref designators
1256
+
1257
+ subparts = p2.latest().assembly.subparts.all()
1258
+
1259
+ self.assertEqual(subparts[0].part_revision.part.full_part_number(), '3333')
1260
+ self.assertEqual(subparts[0].count, 4)
1261
+ self.assertEqual(subparts[1].part_revision.part.full_part_number(), '500-5555-00')
1262
+ self.assertEqual(subparts[1].reference, 'U3, IC2, IC3')
1263
+ self.assertEqual(subparts[1].count, 3)
1264
+ self.assertEqual(subparts[1].do_not_load, False)
1265
+ self.assertEqual(subparts[2].part_revision.part.full_part_number(), '500-5555-00')
1266
+ self.assertEqual(subparts[2].reference, 'R1, R2')
1267
+ self.assertEqual(subparts[2].count, 2)
1268
+ self.assertEqual(subparts[2].do_not_load, True)
1269
+
1270
+ def test_upload_parts(self):
1271
+ create_some_fake_part_classes(self.organization)
1272
+
1273
+ # part_count = Part.objects.all().count()
1274
+ # Should pass
1275
+ with open(f'{TEST_FILES_DIR}/test_new_parts_5_intelligent.csv') as test_csv:
1276
+ response = self.client.post(reverse('bom:upload-parts'), {'file': test_csv})
1277
+ self.assertEqual(response.status_code, 302)
1278
+ new_part_count = Part.objects.all().count()
1279
+ self.assertEqual(new_part_count, 4)
1280
+
1281
+ # Part should be skipped because it already exists
1282
+ with open(f'{TEST_FILES_DIR}/test_new_parts_5_intelligent.csv') as test_csv:
1283
+ response = self.client.post(reverse('bom:upload-parts'), {'file': test_csv})
1284
+ self.assertEqual(response.status_code, 302)
1285
+ found_error = False
1286
+ for m in response.wsgi_request._messages:
1287
+ if "already exists" in str(m):
1288
+ found_error = True
1289
+ self.assertTrue(found_error)
1290
+
1291
+ # Only one part should exist
1292
+ self.assertEqual(Part.objects.filter(number_item='C0402X5R10V001').count(), 1)
1293
+
1294
+ # Uploading this BOM should work, and multiple parts should not be created
1295
+ p = Part.objects.first()
1296
+ with open(f'{TEST_FILES_DIR}/test_bom_5_intelligent.csv') as test_csv:
1297
+ response = self.client.post(reverse('bom:part-upload-bom', kwargs={'part_id': p.id}), {'file': test_csv}, follow=True)
1298
+ self.assertEqual(response.status_code, 200)
1299
+
1300
+ messages = list(response.context.get('messages'))
1301
+ for msg in messages:
1302
+ self.assertTrue("This should not happen." not in msg.message, msg=msg.message)
1303
+
1304
+ def test_upload_part_with_sellers(self):
1305
+ # Should pass
1306
+ initial_parts_count = Part.objects.all().count()
1307
+ with open('bom/test_files/test_new_parts_sellers_intelligent.csv') as test_csv:
1308
+ response = self.client.post(reverse('bom:upload-parts'), {'file': test_csv})
1309
+ self.assertEqual(response.status_code, 302)
1310
+
1311
+ parts_count = Part.objects.all().count()
1312
+ self.assertEqual(parts_count - initial_parts_count, 4)
1313
+
1314
+ @skip('not applicable')
1315
+ def test_upload_part_classes(self):
1316
+ pass
1317
+
1318
+ @skip('not applicable')
1319
+ def test_part_upload_bom_corner_cases(self):
1320
+ pass
1321
+
1322
+ def test_upload_part_classes_parts_and_boms(self):
1323
+ # TODO: Make this more robust
1324
+ self.organization.number_item_len = 5
1325
+ self.organization.save()
1326
+
1327
+ with open(f'{TEST_FILES_DIR}/test_new_parts_5_intelligent.csv') as test_csv:
1328
+ response = self.client.post(reverse('bom:upload-parts'), {'file': test_csv}, follow=True)
1329
+ messages = list(response.context.get('messages'))
1330
+ for msg in messages:
1331
+ self.assertEqual(msg.tags, 'info')
1332
+
1333
+ self.assertEqual(response.status_code, 200)
1334
+ new_part_count = Part.objects.all().count()
1335
+ self.assertEqual(new_part_count, 4)
1336
+
1337
+ pcba = Part.objects.get(number_item='DYSON-123')
1338
+
1339
+ with open(f'{TEST_FILES_DIR}/test_bom_5_intelligent.csv') as test_csv:
1340
+ response = self.client.post(reverse('bom:part-upload-bom', kwargs={'part_id': pcba.id}), {'file': test_csv}, follow=True)
1341
+ self.assertEqual(response.status_code, 200)
1342
+
1343
+ messages = list(response.context.get('messages'))
1344
+
1345
+ for msg in messages:
1346
+ self.assertNotEqual(msg.tags, "error")
1347
+ self.assertEqual(msg.tags, "info")
1348
+
1349
+ subparts = pcba.latest().assembly.subparts.all().order_by('id')
1350
+ self.assertEqual(subparts[0].reference, 'C1, C2, C3')
1351
+ self.assertEqual(subparts[1].reference, 'C4, C5')
1352
+ self.assertEqual(subparts[2].reference, '')
1353
+
1354
+ pt1, pt2, pt3, pt4 = create_some_fake_parts(self.organization)
1355
+ with open(f'{TEST_FILES_DIR}/test_bom_5_intelligent_no_reference.csv') as test_csv:
1356
+ response = self.client.post(reverse('bom:part-upload-bom', kwargs={'part_id': pt1.id}), {'file': test_csv}, follow=True)
1357
+ self.assertEqual(response.status_code, 200)
1358
+ subparts = pt1.latest().assembly.subparts.all().order_by('id')
1359
+ self.assertNotEqual(subparts[0].count, 0)
1360
+ self.assertNotEqual(subparts[1].count, 0)
1361
+ self.assertNotEqual(subparts[2].count, 0)
1362
+
1363
+ @override_settings(BOM_CONFIG=settings.BOM_CONFIG_DEFAULT)
1364
+ class TestBOMNoVariation(TestBOM):
1365
+ def setUp(self):
1366
+ self.client = Client()
1367
+ self.user, self.organization = create_user_and_organization()
1368
+ self.profile = self.user.bom_profile(organization=self.organization)
1369
+ self.organization.number_variation_len = 0
1370
+ self.organization.save()
1371
+ self.client.login(username='kasper', password='ghostpassword')
1372
+
1373
+ @skip('not applicable')
1374
+ def test_create_part_variation(self):
1375
+ pass
1376
+
1377
+ @skip('too specific of a test case for now...')
1378
+ def test_upload_part_classes_parts_and_boms(self):
1379
+ pass
1380
+
1381
+ @skip('not applicable')
1382
+ def test_part_upload_bom_corner_cases(self):
1383
+ pass
1384
+
1385
+ @override_settings(BOM_CONFIG=settings.BOM_CONFIG_DEFAULT)
1386
+ class TestForms(TestCase):
1387
+ def setUp(self):
1388
+ self.client = Client()
1389
+ self.user = User.objects.create_user('kasper', 'kasper@McFadden.com', 'ghostpassword')
1390
+ self.organization = create_a_fake_organization(self.user)
1391
+ self.profile = self.user.bom_profile(organization=self.organization)
1392
+
1393
+ def test_part_info_form(self):
1394
+ form_data = {'quantity': 10}
1395
+ form = PartInfoForm(data=form_data)
1396
+ self.assertTrue(form.is_valid())
1397
+
1398
+ def test_part_info_form_blank(self):
1399
+ form = PartInfoForm({})
1400
+ self.assertFalse(form.is_valid())
1401
+ self.assertEqual(form.errors, {
1402
+ 'quantity': [u'This field is required.'],
1403
+ })
1404
+
1405
+ def test_part_form(self):
1406
+ (pc1, pc2, pc3) = create_some_fake_part_classes(self.organization)
1407
+ form_data = {
1408
+ 'number_class': str(pc1),
1409
+ 'description': "ASSY, ATLAS WRISTBAND 10",
1410
+ 'revision': 'AA'
1411
+ }
1412
+
1413
+ form = PartFormSemiIntelligent(data=form_data, organization=self.organization)
1414
+ self.assertTrue(form.is_valid())
1415
+
1416
+ (m1, m2, m3) = create_some_fake_manufacturers(self.organization)
1417
+
1418
+ form_data = {
1419
+ 'number_class': str(pc2),
1420
+ 'description': "ASSY, ATLAS WRISTBAND 5",
1421
+ 'revision': '1',
1422
+ }
1423
+
1424
+ form = PartFormSemiIntelligent(data=form_data, organization=self.organization)
1425
+ self.assertTrue(form.is_valid())
1426
+
1427
+ new_part, created = Part.objects.get_or_create(
1428
+ number_class=form.cleaned_data['number_class'],
1429
+ number_item=form.cleaned_data['number_item'],
1430
+ number_variation=form.cleaned_data['number_variation'],
1431
+ organization=self.organization)
1432
+
1433
+ self.assertTrue(created)
1434
+ self.assertEqual(new_part.number_class.id, pc2.id)
1435
+
1436
+ def test_part_form_blank(self):
1437
+ (pc1, pc2, pc3) = create_some_fake_part_classes(self.organization)
1438
+
1439
+ form = PartFormSemiIntelligent(data={}, organization=self.organization)
1440
+
1441
+ self.assertFalse(form.is_valid())
1442
+ self.assertEqual(form.errors, {
1443
+ 'number_class': [u'This field is required.'],
1444
+ })
1445
+
1446
+ def test_add_subpart_form(self):
1447
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
1448
+
1449
+ form_data = {'subpart_part_number': p1.full_part_number(), 'count': 10, 'reference': '', 'do_not_load': False}
1450
+ form = AddSubpartForm(organization=self.organization, data=form_data, part_id=p2.id)
1451
+ self.assertTrue(form.is_valid())
1452
+
1453
+ def test_add_subpart_form_blank(self):
1454
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
1455
+
1456
+ form = AddSubpartForm({}, organization=self.organization, part_id=p1.id)
1457
+ self.assertFalse(form.is_valid())
1458
+ self.assertTrue('subpart_part_number' in str(form.errors))
1459
+ self.assertTrue('This field is required.' in str(form.errors))
1460
+
1461
+ def test_add_sellerpart_form(self):
1462
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
1463
+ form = SellerPartForm()
1464
+ self.assertFalse(form.is_valid())
1465
+
1466
+ seller = Seller.objects.filter(organization=self.organization)[0]
1467
+
1468
+ form_data = {
1469
+ 'seller': seller.id,
1470
+ 'seller_part_number': '123-45678',
1471
+ 'minimum_order_quantity': 1000,
1472
+ 'minimum_pack_quantity': 100,
1473
+ 'unit_cost': 1.2332,
1474
+ 'lead_time_days': 14,
1475
+ 'nre_cost': 1000,
1476
+ 'ncnr': True,
1477
+ }
1478
+
1479
+ filled_form = SellerPartForm(form_data, organization=self.organization)
1480
+ self.assertTrue(filled_form.is_valid())
1481
+
1482
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
1483
+ sp = p1.optimal_seller()
1484
+ sp.unit_cost = 10
1485
+ sp.nre_cost = 22
1486
+ sp.save()
1487
+
1488
+ filled_form = SellerPartForm(instance=sp, organization=self.organization)
1489
+ self.assertFalse("$10.0" in filled_form.as_ul())
1490
+ self.assertFalse("$22.0" in filled_form.as_ul())
1491
+
1492
+ @override_settings(BOM_CONFIG=settings.BOM_CONFIG_DEFAULT)
1493
+ class TestJsonViews(TestCase):
1494
+ def setUp(self):
1495
+ self.client = Client()
1496
+ self.user = User.objects.create_user('kasper', 'kasper@McFadden.com', 'ghostpassword')
1497
+ self.organization = create_a_fake_organization(self.user)
1498
+ self.profile = self.user.bom_profile(organization=self.organization)
1499
+ self.client.login(username='kasper', password='ghostpassword')
1500
+
1501
+ def test_mouser_part_match_bom(self):
1502
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
1503
+ self.assertGreaterEqual(len(p3.latest().assembly.subparts.all()), 1)
1504
+ response = self.client.get(reverse('json:mouser-part-match-bom', kwargs={'part_revision_id': p3.latest().id}))
1505
+
1506
+ self.assertEqual(response.status_code, 200)