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/helpers.py ADDED
@@ -0,0 +1,367 @@
1
+ from bom import constants
2
+ from bom.models import (
3
+ Assembly,
4
+ Manufacturer,
5
+ ManufacturerPart,
6
+ Organization,
7
+ Part,
8
+ PartClass,
9
+ PartRevision,
10
+ QuantityOfMeasure,
11
+ UnitDefinition,
12
+ PartRevisionProperty,
13
+ PartRevisionPropertyDefinition,
14
+ Seller,
15
+ SellerPart,
16
+ Subpart,
17
+ User,
18
+ )
19
+
20
+
21
+ def create_a_fake_organization(user, free=False, number_scheme=constants.NUMBER_SCHEME_SEMI_INTELLIGENT, number_variation_len=constants.NUMBER_VARIATION_LEN_DEFAULT):
22
+ org, created = Organization.objects.get_or_create(
23
+ name="Atlas",
24
+ subscription=constants.SUBSCRIPTION_TYPE_FREE if free else constants.SUBSCRIPTION_TYPE_PRO,
25
+ number_scheme=number_scheme,
26
+ number_item_len=4,
27
+ number_variation_len=number_variation_len,
28
+ owner=user)
29
+ return org
30
+
31
+
32
+ def create_user_and_organization(free=False, number_scheme=constants.NUMBER_SCHEME_SEMI_INTELLIGENT, number_variation_len=constants.NUMBER_VARIATION_LEN_DEFAULT):
33
+ user = User.objects.create_user('kasper', 'kasper@McFadden.com', 'ghostpassword')
34
+ organization = create_a_fake_organization(user, free, number_scheme, number_variation_len)
35
+ profile = user.bom_profile(organization=organization)
36
+ profile.role = 'A'
37
+ profile.save()
38
+ return user, organization
39
+
40
+
41
+ def create_some_fake_part_classes(organization):
42
+ pc1, c = PartClass.objects.get_or_create(code=500, name='Wendy', comment='Mechanical Switches', organization=organization)
43
+ pc2, c = PartClass.objects.get_or_create(code=200, name='Archibald', comment='', organization=organization)
44
+ pc3, c = PartClass.objects.get_or_create(code='50A', name='Ghost', comment='Like Kasper', organization=organization)
45
+ return pc1, pc2, pc3
46
+
47
+
48
+ def create_a_fake_subpart(part_revision, reference="U1", count=4):
49
+ sp1 = Subpart(
50
+ part_revision=part_revision,
51
+ reference=reference,
52
+ count=count)
53
+ sp1.save()
54
+
55
+ return sp1
56
+
57
+
58
+ def create_a_fake_assembly():
59
+ assy = Assembly.objects.create()
60
+ return assy
61
+
62
+
63
+ def create_a_fake_assembly_with_subpart(part_revision, reference="D4", count=4):
64
+ assy = create_a_fake_assembly()
65
+ subpart = create_a_fake_subpart(part_revision, reference, count)
66
+ assy.subparts.add(subpart)
67
+ return assy
68
+
69
+
70
+ def create_some_fake_quantities_of_measure(organization=None):
71
+ qom_each, _ = QuantityOfMeasure.objects.get_or_create(name='Each')
72
+ qom_volt, _ = QuantityOfMeasure.objects.get_or_create(name='Voltage')
73
+ qom_custom = None
74
+ if organization is not None:
75
+ qom_custom, _ = QuantityOfMeasure.objects.get_or_create(name='Custom', organization=organization)
76
+ return qom_each, qom_volt, qom_custom
77
+
78
+
79
+ def create_some_fake_unit_definitions(organization=None):
80
+ qom_each, qom_volt, qom_custom = create_some_fake_quantities_of_measure(organization)
81
+ v, _ = UnitDefinition.objects.get_or_create(name='Volts', symbol='V', quantity_of_measure=qom_volt,
82
+ base_multiplier=1.0)
83
+ mv, _ = UnitDefinition.objects.get_or_create(name='Millivolts', symbol='mV', quantity_of_measure=qom_volt,
84
+ base_multiplier=0.001)
85
+ uv, _ = UnitDefinition.objects.get_or_create(name='Microvolts', symbol='uV', quantity_of_measure=qom_volt,
86
+ base_multiplier=0.000001)
87
+ kv, _ = UnitDefinition.objects.get_or_create(name='Kilovolts', symbol='kV', quantity_of_measure=qom_volt,
88
+ base_multiplier=1000)
89
+ sv = None
90
+ if organization is not None:
91
+ sv, _ = UnitDefinition.objects.get_or_create(name='Supervolts', symbol='SV', quantity_of_measure=qom_custom,
92
+ base_multiplier=1000000)
93
+ return v, mv, uv, kv, sv
94
+
95
+
96
+ def create_some_fake_part_revision_property_definitions(organization=None, some_required=True, part_class=None):
97
+ qom_each, qom_volt, qom_custom = create_some_fake_quantities_of_measure(organization)
98
+ sheen, _ = PartRevisionPropertyDefinition.objects.get_or_create(name='Sheen', code='sheen',
99
+ type=constants.PART_REVISION_PROPERTY_TYPE_STRING,
100
+ quantity_of_measure=None,
101
+ defaults={'required': some_required})
102
+ voltage, _ = PartRevisionPropertyDefinition.objects.get_or_create(name='Voltage', code='voltage',
103
+ type=constants.PART_REVISION_PROPERTY_TYPE_DECIMAL,
104
+ quantity_of_measure=qom_volt,
105
+ defaults={'required': some_required})
106
+ max_voltage, _ = PartRevisionPropertyDefinition.objects.get_or_create(name='Max Voltage', code='max_voltage',
107
+ type=constants.PART_REVISION_PROPERTY_TYPE_DECIMAL,
108
+ required=False, quantity_of_measure=qom_volt)
109
+ count, _ = PartRevisionPropertyDefinition.objects.get_or_create(name='Count', code='count',
110
+ type=constants.PART_REVISION_PROPERTY_TYPE_DECIMAL,
111
+ required=False, quantity_of_measure=qom_each)
112
+
113
+ defs = [sheen, voltage, max_voltage, count]
114
+
115
+ custom = None
116
+ if organization is not None:
117
+ custom, _ = PartRevisionPropertyDefinition.objects.get_or_create(name='Custom', code='custom',
118
+ type=constants.PART_REVISION_PROPERTY_TYPE_BOOLEAN,
119
+ required=False, quantity_of_measure=qom_custom)
120
+ defs.append(custom)
121
+
122
+ if part_class is not None:
123
+ part_class.property_definitions.add(*defs)
124
+
125
+ return sheen, voltage, max_voltage, count, custom
126
+
127
+
128
+ def create_a_fake_part_revision(part, assembly, description="Brown dog", revision="1", some_required_definitions=True):
129
+ pch, created = PartRevision.objects.get_or_create(part=part, revision=revision, defaults={
130
+ 'description': description,
131
+ 'revision': revision,
132
+ 'assembly': assembly,
133
+ })
134
+
135
+ organization = part.organization
136
+ part_class = part.number_class if part else None
137
+ _, voltage, max_voltage, count, custom = create_some_fake_part_revision_property_definitions(organization,
138
+ some_required_definitions,
139
+ part_class=part_class)
140
+ v, mv, _, _, sv = create_some_fake_unit_definitions(organization)
141
+
142
+ PartRevisionProperty.objects.get_or_create(part_revision=pch, property_definition=voltage, unit_definition=v,
143
+ value_raw=".01")
144
+ PartRevisionProperty.objects.get_or_create(part_revision=pch, property_definition=max_voltage, unit_definition=v,
145
+ value_raw="3.5")
146
+ PartRevisionProperty.objects.get_or_create(part_revision=pch, property_definition=count, value_raw="5.2")
147
+ PartRevisionProperty.objects.get_or_create(part_revision=pch, property_definition=custom, unit_definition=sv,
148
+ value_raw="True")
149
+
150
+ return pch
151
+
152
+
153
+ def create_some_fake_sellers(organization):
154
+ s1, c = Seller.objects.get_or_create(name='Mouser', organization=organization)
155
+ s2, c = Seller.objects.get_or_create(name='Digi-Key', organization=organization)
156
+ s3, c = Seller.objects.get_or_create(name='Archibald', organization=organization)
157
+ return s1, s2, s3
158
+
159
+
160
+ def create_some_fake_manufacturers(organization):
161
+ m1, c = Manufacturer.objects.get_or_create(name='STMicroelectronics', organization=organization)
162
+ m2, c = Manufacturer.objects.get_or_create(name='Nordic Semiconductor', organization=organization)
163
+ m3, c = Manufacturer.objects.get_or_create(name='Murata', organization=organization)
164
+ return m1, m2, m3
165
+
166
+
167
+ def create_a_fake_seller_part(
168
+ seller,
169
+ manufacturer_part,
170
+ moq,
171
+ mpq,
172
+ unit_cost,
173
+ lead_time_days,
174
+ nre_cost=None):
175
+ sp1 = SellerPart(
176
+ seller=seller,
177
+ manufacturer_part=manufacturer_part,
178
+ minimum_order_quantity=moq,
179
+ minimum_pack_quantity=mpq,
180
+ unit_cost=unit_cost,
181
+ lead_time_days=lead_time_days,
182
+ nre_cost=nre_cost)
183
+ sp1.save()
184
+
185
+ return sp1
186
+
187
+
188
+ def create_some_fake_intelligent_parts(organization):
189
+ pt1 = Part(number_item=('3' * organization.number_item_len), organization=organization)
190
+ pt1.save()
191
+
192
+ pt2 = Part(number_item='4' * organization.number_item_len, organization=organization)
193
+ pt2.save()
194
+
195
+ pt3 = Part(number_item='A' * organization.number_item_len, organization=organization)
196
+ pt3.save()
197
+
198
+ # pt4 is a part with no PartRevision
199
+ pt4 = Part(number_item='B' * organization.number_item_len, organization=organization)
200
+ pt4.save(no_part_revision=True)
201
+
202
+ pt5 = Part(number_item='5' * organization.number_item_len, organization=organization)
203
+ pt5.save()
204
+ return pt1, pt2, pt3, pt4, pt5
205
+
206
+
207
+ def create_some_fake_semi_intelligent_parts(organization):
208
+ (pc1, pc2, pc3) = create_some_fake_part_classes(organization=organization)
209
+ pt1 = Part(number_class=pc2, number_item='3333', organization=organization)
210
+ pt1.save()
211
+
212
+ pt2 = Part(number_class=pc1, organization=organization)
213
+ pt2.save()
214
+
215
+ pt3 = Part(number_class=pc3, organization=organization)
216
+ pt3.save()
217
+
218
+ # pt4 is a part with no PartRevision
219
+ pt4 = Part(number_class=pc1, number_item='4444', organization=organization)
220
+ pt4.save(no_part_revision=True)
221
+
222
+ pt5 = Part(number_class=pc1, number_item='5555', organization=organization)
223
+ pt5.save()
224
+ return pt1, pt2, pt3, pt4, pt5
225
+
226
+
227
+ def create_some_fake_parts(organization):
228
+ (m1, m2, m3) = create_some_fake_manufacturers(organization=organization)
229
+
230
+ if organization.number_scheme == 'I':
231
+ pt1, pt2, pt3, pt4, pt5 = create_some_fake_intelligent_parts(organization)
232
+ elif organization.number_scheme == 'S':
233
+ pt1, pt2, pt3, pt4, pt5 = create_some_fake_semi_intelligent_parts(organization)
234
+ else:
235
+ return None
236
+
237
+ mp1 = ManufacturerPart(part=pt1, manufacturer=m1, manufacturer_part_number='STM32F401CEU6')
238
+ mp1.save()
239
+ pt1.primary_manufacturer_part = mp1
240
+ pt1.save()
241
+ assy = create_a_fake_assembly()
242
+ pr1 = create_a_fake_part_revision(part=pt1, assembly=None)
243
+
244
+ mp2 = ManufacturerPart(part=pt2, manufacturer=m2, manufacturer_part_number='GRM1555C1H100JA01D')
245
+ mp2.save()
246
+ mp22 = ManufacturerPart(part=pt2, manufacturer=None, manufacturer_part_number='ThisPartHasNoManufacturer')
247
+ mp22.save()
248
+ pt2.primary_manufacturer_part = mp2
249
+ pt2.save()
250
+ assy2 = create_a_fake_assembly_with_subpart(part_revision=pr1)
251
+ pr2 = create_a_fake_part_revision(part=pt2, assembly=assy2)
252
+
253
+ mp3 = ManufacturerPart(part=pt3, manufacturer=m3, manufacturer_part_number='NRF51822')
254
+ mp3.save()
255
+ assy3 = create_a_fake_assembly_with_subpart(part_revision=pr2)
256
+ subpart3 = create_a_fake_subpart(pr1, count=10, reference="")
257
+ subpart32 = create_a_fake_subpart(pr2, count=3, reference="")
258
+ assy3.subparts.add(subpart3)
259
+ assy3.subparts.add(subpart32)
260
+ assy3.save()
261
+ create_a_fake_part_revision(part=pt3, assembly=assy3)
262
+ create_a_fake_part_revision(part=pt3, assembly=assy3, revision="2")
263
+
264
+ # Create a part with a PartRevision with no assembly - no longer happens due to PartRevision save override
265
+ create_a_fake_part_revision(pt5, None)
266
+
267
+ (s1, s2, s3) = create_some_fake_sellers(organization=organization)
268
+
269
+ create_a_fake_seller_part(s1, mp1, moq=0, mpq=1, unit_cost=0, lead_time_days=None, nre_cost=0,)
270
+ create_a_fake_seller_part(s1, mp1, moq=1, mpq=2, unit_cost=10, lead_time_days=None, nre_cost=0, )
271
+ create_a_fake_seller_part(s1, mp1, moq=1, mpq=1, unit_cost=1.2, lead_time_days=20, nre_cost=500)
272
+ create_a_fake_seller_part(s2, mp1, moq=1000, mpq=5000, unit_cost=0.1005, lead_time_days=7, nre_cost=0)
273
+ create_a_fake_seller_part(s2, mp2, moq=200, mpq=200, unit_cost=0.5, lead_time_days=47, nre_cost=1)
274
+ create_a_fake_seller_part(s2, mp2, moq=2000, mpq=200, unit_cost=0.4, lead_time_days=47, nre_cost=10)
275
+ create_a_fake_seller_part(s1, mp2, moq=2000, mpq=200, unit_cost=0.4, lead_time_days=47, nre_cost=10)
276
+ create_a_fake_seller_part(s1, mp2, moq=3000, mpq=200, unit_cost=0.3, lead_time_days=47, nre_cost=10)
277
+ return pt1, pt2, pt3, pt4
278
+
279
+
280
+ def create_some_fake_data(user):
281
+ o = create_a_fake_organization(user)
282
+ return create_some_fake_parts(o)
283
+
284
+
285
+ def create_all_part_classes():
286
+ PartClass.objects.get_or_create(code=100, name='Assembly, Top Level SKU\'s, Finished Goods',
287
+ comment='Ready to ship product; Includes packout and literature')
288
+ PartClass.objects.get_or_create(code=101, name='Assembly, Final, Elec/Mech',
289
+ comment='Fully built product without Literature or packout materials')
290
+ PartClass.objects.get_or_create(code=102, name='Assembly, Sub-Assy, Elec/Mech', comment='')
291
+ PartClass.objects.get_or_create(code=103, name='Assembly, Cable', comment='')
292
+ PartClass.objects.get_or_create(code=104, name='Assembly, Packaging', comment='')
293
+ PartClass.objects.get_or_create(code=105, name='Kit, Spare/Upgrade',
294
+ comment='Includes Spares, Documentation Kits, Upgrade Kits')
295
+ PartClass.objects.get_or_create(code=106, name='Assembly, Printed Circuit Board (PBCA)', comment='')
296
+ PartClass.objects.get_or_create(code=107, name='PCB, Fab', comment='')
297
+ PartClass.objects.get_or_create(code=108, name='Gerber File/X-Y data/CAD data', comment='')
298
+ PartClass.objects.get_or_create(code=109, name='Schematic/Test Diagram', comment='')
299
+ PartClass.objects.get_or_create(code=110, name='Mechanical Reference, PCB', comment='')
300
+ PartClass.objects.get_or_create(code=111, name='Mechanical Reference, PCBA', comment='')
301
+ PartClass.objects.get_or_create(code=112, name='Programmed Device, Firmware/Software', comment='')
302
+ PartClass.objects.get_or_create(code=113, name='Program File, Firmware, Software', comment='')
303
+ PartClass.objects.get_or_create(code=114, name='Build-To-Print, Sheet Metal', comment='')
304
+ PartClass.objects.get_or_create(code=115, name='Build-To-Print, Cast Metal', comment='')
305
+ PartClass.objects.get_or_create(code=116, name='Build-To-Print, Extruded',
306
+ comment='Applies for both plastic and metal')
307
+ PartClass.objects.get_or_create(code=117, name='Build-To-Print, Machined',
308
+ comment='Applies for both plastic and metal')
309
+ PartClass.objects.get_or_create(code=118, name='Build-To-Print, Molded',
310
+ comment='Applies for both Injection and Compression molded')
311
+ PartClass.objects.get_or_create(code=119, name='Build-To-Print, Formed',
312
+ comment='Applies for plastic only; Formed sheet metal goes under sheet metal')
313
+ PartClass.objects.get_or_create(code=120, name='Build-To-Print, Die-Cut',
314
+ comment='Applied to foams and PSAs (Pressure Sensitive Adhesives)')
315
+ PartClass.objects.get_or_create(code=121, name='Build-To-Print, Mechanical',
316
+ comment='Applies to all Build-To-Print Mech Parts that do not fall into one of the other specific groups.')
317
+ PartClass.objects.get_or_create(code=122, name='Build-To-Print, Printed Material',
318
+ comment='Includes Custom Labels, Product Literature, etc.')
319
+ PartClass.objects.get_or_create(code=123, name='Build-To-Print, Packaging', comment='')
320
+ PartClass.objects.get_or_create(code=124, name='Drawing',
321
+ comment='For any drawing that does not use the Item PN as the dwg PN')
322
+ PartClass.objects.get_or_create(code=125, name='Artwork',
323
+ comment='A/W for printing, silkscreening of labels, sheetmetal, etc.')
324
+ PartClass.objects.get_or_create(code=126, name='Release Notes/Protocols', comment='')
325
+ PartClass.objects.get_or_create(code=127, name='Custom Electronic Components', comment='Example: Custom Sensors')
326
+ PartClass.objects.get_or_create(code=200, name='Adhesive', comment='Includes Loctite, Epoxy, Tape, etc.')
327
+ PartClass.objects.get_or_create(code=201, name='Battery/Charger', comment='')
328
+ PartClass.objects.get_or_create(code=202, name='Cable & Wire', comment='')
329
+ PartClass.objects.get_or_create(code=203, name='Connector, Cable/Harness', comment='')
330
+ PartClass.objects.get_or_create(code=204, name='Connector, PC Mountable', comment='')
331
+ PartClass.objects.get_or_create(code=205, name='Connector, Misc', comment='')
332
+ PartClass.objects.get_or_create(code=206, name='Cable Hardware', comment='Includes Clamps, Ties, etc.')
333
+ PartClass.objects.get_or_create(code=207, name='Capacitor', comment='')
334
+ PartClass.objects.get_or_create(code=208, name='Circuit Breaker/Filter', comment='')
335
+ PartClass.objects.get_or_create(code=209, name='Crystal', comment='')
336
+ PartClass.objects.get_or_create(code=210, name='Delay Line', comment='')
337
+ PartClass.objects.get_or_create(code=211, name='Diode', comment='')
338
+ PartClass.objects.get_or_create(code=212, name='Fan & Fan Accessories', comment='')
339
+ PartClass.objects.get_or_create(code=213, name='Fuse & Fuse Hardware', comment='')
340
+ PartClass.objects.get_or_create(code=214, name='Hardware',
341
+ comment='Includes Screws, Nuts, Washers, Springs, Std-offs, Inserts, Fasteners, etc')
342
+ PartClass.objects.get_or_create(code=215, name='Heatsink', comment='')
343
+ PartClass.objects.get_or_create(code=216, name='IC', comment='')
344
+ PartClass.objects.get_or_create(code=217, name='Inductor', comment='')
345
+ PartClass.objects.get_or_create(code=218, name='Insulator', comment='')
346
+ PartClass.objects.get_or_create(code=219, name='Label', comment='')
347
+ PartClass.objects.get_or_create(code=220, name='Led', comment='/Light')
348
+ PartClass.objects.get_or_create(code=221, name='Packaging Material',
349
+ comment='Includes Bags, Boxes, Pallets, etc. Non-Custom only.')
350
+ PartClass.objects.get_or_create(code=222, name='Rectifier', comment='')
351
+ PartClass.objects.get_or_create(code=223, name='Resistor', comment='')
352
+ PartClass.objects.get_or_create(code=224, name='Resistor Network', comment='')
353
+ PartClass.objects.get_or_create(code=225, name='Socket', comment='')
354
+ PartClass.objects.get_or_create(code=226, name='Switch', comment='')
355
+ PartClass.objects.get_or_create(code=227, name='Terminal', comment='Includes Ring, Spade, Butt, etc.')
356
+ PartClass.objects.get_or_create(code=228, name='Transformer', comment='')
357
+ PartClass.objects.get_or_create(code=229, name='Transistor', comment='')
358
+ PartClass.objects.get_or_create(code=230, name='Tubing', comment='All types')
359
+ PartClass.objects.get_or_create(code=231, name='Sensor', comment='')
360
+ PartClass.objects.get_or_create(code=232, name='Power Supply', comment='')
361
+ PartClass.objects.get_or_create(code=233, name='Enclosures', comment='Off-The-Shelf Enclosures')
362
+ PartClass.objects.get_or_create(code=234, name='Varistor', comment='')
363
+ PartClass.objects.get_or_create(code=235, name='Ferrites', comment='')
364
+ PartClass.objects.get_or_create(code=236, name='Suppressor', comment='')
365
+ PartClass.objects.get_or_create(code=237, name='Misc Material', comment='')
366
+ PartClass.objects.get_or_create(code=238, name='Electronic Assy (Non-Custom)', comment='')
367
+ PartClass.objects.get_or_create(code=239, name='Antenna', comment='')
bom/local_settings.py ADDED
@@ -0,0 +1,35 @@
1
+ from pathlib import Path
2
+
3
+ BASE_DIR = Path(__file__).resolve().parent.parent
4
+
5
+ # SECURITY WARNING: keep the secret key used in production secret!
6
+ SECRET_KEY = 'supersecretkey'
7
+
8
+ # SECURITY WARNING: don't run with debug turned on in production!
9
+ DEBUG = True
10
+
11
+ ALLOWED_HOSTS = ['*']
12
+
13
+ BOM_CONFIG = {
14
+ 'mouser_api_key': 'secretkey',
15
+ 'admin_dashboard': {
16
+ 'enable_autocomplete': False,
17
+ 'page_size': 50,
18
+ }
19
+ }
20
+
21
+ # google GoogleOAuth
22
+ SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = 'secretkey'
23
+ SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = 'secretkey'
24
+
25
+ # Database
26
+ # https://docs.djangoproject.com/en/1.10/ref/settings/#databases
27
+
28
+ DATABASES = {
29
+ 'default': {
30
+ 'ENGINE': 'django.db.backends.sqlite3',
31
+ 'NAME': BASE_DIR / 'db.sqlite3',
32
+ }
33
+ }
34
+
35
+ FIXER_ACCESS_KEY = 'secretkey from fixer.io' # for exchange rate conversions
@@ -0,0 +1,135 @@
1
+ # Generated by Django 2.1.1 on 2018-09-06 02:40
2
+
3
+ from django.conf import settings
4
+ import django.core.validators
5
+ from django.db import migrations, models
6
+ import django.db.models.deletion
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ initial = True
12
+
13
+ dependencies = [
14
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15
+ ]
16
+
17
+ operations = [
18
+ migrations.CreateModel(
19
+ name='Manufacturer',
20
+ fields=[
21
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22
+ ('name', models.CharField(default=None, max_length=128)),
23
+ ],
24
+ options={
25
+ 'ordering': ['name'],
26
+ },
27
+ ),
28
+ migrations.CreateModel(
29
+ name='Organization',
30
+ fields=[
31
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
32
+ ('name', models.CharField(default=None, max_length=255)),
33
+ ('subscription', models.CharField(choices=[('F', 'Free'), ('P', 'Pro')], max_length=1)),
34
+ ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
35
+ ],
36
+ ),
37
+ migrations.CreateModel(
38
+ name='Part',
39
+ fields=[
40
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
41
+ ('number_item', models.CharField(blank=True, default=None, max_length=4, validators=[django.core.validators.RegexValidator('^[0-9]*$', 'Only numeric characters are allowed.')])),
42
+ ('number_variation', models.CharField(blank=True, default=None, max_length=2, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z]*$', 'Only alphanumeric characters are allowed.')])),
43
+ ('description', models.CharField(default=None, max_length=255)),
44
+ ('revision', models.CharField(max_length=2)),
45
+ ('manufacturer_part_number', models.CharField(blank=True, default='', max_length=128)),
46
+ ('manufacturer', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='bom.Manufacturer')),
47
+ ],
48
+ ),
49
+ migrations.CreateModel(
50
+ name='PartClass',
51
+ fields=[
52
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
53
+ ('code', models.CharField(max_length=3, unique=True)),
54
+ ('name', models.CharField(default=None, max_length=255)),
55
+ ('comment', models.CharField(blank=True, default=None, max_length=255)),
56
+ ],
57
+ ),
58
+ migrations.CreateModel(
59
+ name='PartFile',
60
+ fields=[
61
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
62
+ ('file', models.FileField(upload_to='partfiles/')),
63
+ ('upload_date', models.DateField(auto_now=True)),
64
+ ('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bom.Part')),
65
+ ],
66
+ ),
67
+ migrations.CreateModel(
68
+ name='Seller',
69
+ fields=[
70
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
71
+ ('name', models.CharField(default=None, max_length=128)),
72
+ ('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bom.Organization')),
73
+ ],
74
+ ),
75
+ migrations.CreateModel(
76
+ name='SellerPart',
77
+ fields=[
78
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
79
+ ('minimum_order_quantity', models.IntegerField(blank=True, null=True)),
80
+ ('minimum_pack_quantity', models.IntegerField(blank=True, null=True)),
81
+ ('unit_cost', models.DecimalField(blank=True, decimal_places=4, max_digits=8, null=True)),
82
+ ('lead_time_days', models.IntegerField(blank=True, null=True)),
83
+ ('nre_cost', models.DecimalField(blank=True, decimal_places=4, max_digits=8, null=True)),
84
+ ('ncnr', models.BooleanField(default=False)),
85
+ ('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bom.Part')),
86
+ ('seller', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bom.Seller')),
87
+ ],
88
+ ),
89
+ migrations.CreateModel(
90
+ name='Subpart',
91
+ fields=[
92
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
93
+ ('count', models.IntegerField(default=1)),
94
+ ('assembly_part', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='assembly_part', to='bom.Part')),
95
+ ('assembly_subpart', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='assembly_subpart', to='bom.Part')),
96
+ ],
97
+ ),
98
+ migrations.CreateModel(
99
+ name='UserMeta',
100
+ fields=[
101
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
102
+ ('role', models.CharField(choices=[('A', 'Admin'), ('V', 'Viewer')], max_length=1)),
103
+ ('organization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='bom.Organization')),
104
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
105
+ ],
106
+ ),
107
+ migrations.AddField(
108
+ model_name='part',
109
+ name='number_class',
110
+ field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.PROTECT, related_name='number_class', to='bom.PartClass'),
111
+ ),
112
+ migrations.AddField(
113
+ model_name='part',
114
+ name='organization',
115
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bom.Organization'),
116
+ ),
117
+ migrations.AddField(
118
+ model_name='part',
119
+ name='subparts',
120
+ field=models.ManyToManyField(blank=True, through='bom.Subpart', to='bom.Part'),
121
+ ),
122
+ migrations.AddField(
123
+ model_name='manufacturer',
124
+ name='organization',
125
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bom.Organization'),
126
+ ),
127
+ migrations.AlterUniqueTogether(
128
+ name='sellerpart',
129
+ unique_together={('seller', 'part', 'minimum_order_quantity', 'unit_cost')},
130
+ ),
131
+ migrations.AlterUniqueTogether(
132
+ name='part',
133
+ unique_together={('number_class', 'number_item', 'number_variation', 'organization')},
134
+ ),
135
+ ]
@@ -0,0 +1,24 @@
1
+ # Generated by Django 2.1.1 on 2018-09-08 21:51
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('bom', '0001_initial'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterField(
15
+ model_name='subpart',
16
+ name='assembly_part',
17
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assembly_part', to='bom.Part'),
18
+ ),
19
+ migrations.AlterField(
20
+ model_name='subpart',
21
+ name='assembly_subpart',
22
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='assembly_subpart', to='bom.Part'),
23
+ ),
24
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 2.1.1 on 2018-09-10 23:42
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('bom', '0002_auto_20180908_2151'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='sellerpart',
15
+ name='data_source',
16
+ field=models.CharField(default=None, max_length=32, null=True),
17
+ ),
18
+ ]
@@ -0,0 +1,18 @@
1
+ # Generated by Django 2.1.1 on 2018-09-11 00:11
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('bom', '0003_sellerpart_data_source'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='sellerpart',
15
+ name='data_source',
16
+ field=models.CharField(blank=True, default=None, max_length=32, null=True),
17
+ ),
18
+ ]
@@ -0,0 +1,56 @@
1
+ # Generated by Django 2.1.1 on 2018-10-07 19:34
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ def update_sellerpart_fk(apps, schema_editor):
8
+ Part = apps.get_model('bom', 'Part')
9
+ SellerPart = apps.get_model('bom', 'SellerPart')
10
+ ManufacturerPart = apps.get_model('bom', 'ManufacturerPart')
11
+
12
+ for sp in SellerPart.objects.all():
13
+ (mp, created) = ManufacturerPart.objects.get_or_create(part=sp.part, manufacturer_part_number=sp.part.manufacturer_part_number, manufacturer=sp.part.manufacturer)
14
+ sp.manufacturer_part = mp
15
+ sp.save()
16
+
17
+ for p in Part.objects.all():
18
+ (mp, created) = ManufacturerPart.objects.get_or_create(part=p, manufacturer_part_number=p.manufacturer_part_number, manufacturer=p.manufacturer)
19
+
20
+
21
+ class Migration(migrations.Migration):
22
+
23
+ dependencies = [
24
+ ('bom', '0004_auto_20180911_0011'),
25
+ ]
26
+
27
+ operations = [
28
+ migrations.CreateModel(
29
+ name='ManufacturerPart',
30
+ fields=[
31
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
32
+ ('manufacturer_part_number', models.CharField(blank=True, default='', max_length=128)),
33
+ ('manufacturer', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='bom.Manufacturer')),
34
+ ('part', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bom.Part')),
35
+ ],
36
+ ),
37
+ migrations.AddField(
38
+ model_name='sellerpart',
39
+ name='manufacturer_part',
40
+ field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='bom.ManufacturerPart'),
41
+ ),
42
+ migrations.RunPython(update_sellerpart_fk),
43
+ migrations.AlterField(
44
+ model_name='sellerpart',
45
+ name='manufacturer_part',
46
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bom.ManufacturerPart'),
47
+ ),
48
+ migrations.AlterUniqueTogether(
49
+ name='sellerpart',
50
+ unique_together={('seller', 'manufacturer_part', 'minimum_order_quantity', 'unit_cost')},
51
+ ),
52
+ migrations.AlterUniqueTogether(
53
+ name='manufacturerpart',
54
+ unique_together={('part', 'manufacturer_part_number', 'manufacturer')},
55
+ ),
56
+ ]