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