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
File without changes
@@ -0,0 +1,51 @@
1
+ import json
2
+ import requests
3
+ import hashlib
4
+
5
+ from django.conf import settings
6
+ from django.core.cache import cache
7
+
8
+
9
+ class BaseApi:
10
+ def __init__(self, api_settings_key, root_url, api_key_query=None, cache_timeout=86400):
11
+ self.api_key = None
12
+ self.root_url = root_url
13
+ self.api_key_query = api_key_query
14
+ self.cache_timeout = cache_timeout
15
+ try:
16
+ self.api_key = settings.BOM_CONFIG[api_settings_key]
17
+ except KeyError as e:
18
+ raise ValueError('No API key for {} found on server. Contact administrator for help.'.format(api_settings_key))
19
+
20
+ def request(self, suburl, data=None):
21
+ cache_key = suburl
22
+ if data is not None:
23
+ data_md5 = hashlib.md5(json.dumps(data, sort_keys=True).encode('utf-8')).hexdigest()
24
+ cache_key += '-{}'.format(data_md5)
25
+ cached_data = cache.get(cache_key)
26
+ if cached_data is not None:
27
+ # print('Found cached data!')
28
+ return cached_data
29
+
30
+ url = self.root_url + suburl
31
+
32
+ if self.api_key_query is None or self.api_key is None:
33
+ raise ValueError('No API key, or api key query found on server. Contact administrator for help.')
34
+
35
+ params = ((self.api_key_query, self.api_key), )
36
+ headers = {'accept': 'application/json', }
37
+
38
+ if data is not None:
39
+ headers.update({'Content-Type': 'application/json'})
40
+ r = requests.post(url, headers=headers, params=params, data=json.dumps(data))
41
+ else:
42
+ r = requests.get(url, headers=headers, params=params)
43
+ if r.status_code != 200:
44
+ raise BaseApiError(f"HTTP Response != 200. Returned: {r.status_code} {r.reason}")
45
+
46
+ cache.set(cache_key, r.content, self.cache_timeout)
47
+ return r.content
48
+
49
+
50
+ class BaseApiError(Exception):
51
+ pass
@@ -0,0 +1,166 @@
1
+ from django.contrib import messages
2
+ from django.contrib.auth.decorators import login_required
3
+ from django.core.exceptions import ObjectDoesNotExist
4
+ from django.http import HttpResponseRedirect
5
+ from django.urls import reverse
6
+
7
+ from google.oauth2.credentials import Credentials
8
+ from googleapiclient.discovery import build
9
+ from googleapiclient.errors import HttpError
10
+ from requests import HTTPError
11
+ from social_django.utils import load_strategy
12
+
13
+ from bom.decorators import google_authenticated
14
+ from bom.models import Part
15
+
16
+
17
+ # Helpers
18
+ def get_service(user):
19
+ social = user.social_auth.get(provider='google-oauth2')
20
+ ls = load_strategy()
21
+ access_token = social.get_access_token(ls)
22
+ credentials = Credentials(access_token)
23
+ service = build('drive', 'v3', credentials=credentials)
24
+ return service
25
+
26
+
27
+ def create_root(user):
28
+ organization = user.bom_profile().organization
29
+ service = get_service(user)
30
+ file_metadata = {
31
+ 'name': 'IndaBOM Part Files',
32
+ 'mimeType': 'application/vnd.google-apps.folder',
33
+ 'folderColorRgb': 'green',
34
+ }
35
+ file = service.files().create(body=file_metadata, fields='id').execute()
36
+ organization.google_drive_parent = file.get('id')
37
+ organization.save()
38
+ return organization.google_drive_parent
39
+
40
+
41
+ def create_part_folder(user, part):
42
+ service = get_service(user)
43
+ print("got service")
44
+ organization = user.bom_profile().organization
45
+ file_metadata = {
46
+ 'name': part.full_part_number() + ' ' + part.latest().description,
47
+ 'mimeType': 'application/vnd.google-apps.folder',
48
+ 'parents': [organization.google_drive_parent],
49
+ }
50
+ print("about to create")
51
+ file = service.files().create(body=file_metadata, fields='id').execute()
52
+ part.google_drive_parent = file.get('id')
53
+ part.save()
54
+ return part.google_drive_parent
55
+
56
+
57
+ def get_files_list(user, part):
58
+ # TODO: Figure this out...
59
+ service = get_service(user)
60
+ response = service.files().list(q="'{}' in parents".format(part.google_drive_parent),
61
+ fields='files(id, name)').execute()
62
+ return response
63
+
64
+
65
+ # Social Auth Pipeline
66
+ def initialize_parent(backend, user, response, *args, **kwargs):
67
+ if backend.name == 'google-oauth2':
68
+ # Only the owner can create the root folder
69
+ if user.bom_profile().organization.owner is user:
70
+ create_root(user)
71
+
72
+
73
+ def uninitialize_parent(backend, user, *args, **kwargs):
74
+ if backend.name == 'google-oauth2':
75
+ organization = user.bom_profile().organization
76
+ # Do nothing for now, let's not deactivate the folder, maybe the user wants it back later,
77
+ # Let's instead run a try/catch when we get the parent folder given the stored ID, if there's an error, we create
78
+ # Only the owner can create the root folder
79
+ # if user.bom_profile().organization.owner is user:
80
+ # service = get_service(user)
81
+ # file = service.files().get(fileId=organization.google_drive_parent).execute()
82
+ # inactive_filename = file['name'] + '(inactive {})'.format(datetime.now())
83
+ # service.files().update(fileId=organization.google_drive_parent, body={'name': inactive_filename}).execute()
84
+
85
+
86
+ # Views
87
+ @login_required
88
+ @google_authenticated
89
+ def get_or_create_and_open_folder(request, part_id):
90
+ user = request.user
91
+ organization = user.bom_profile().organization
92
+ try:
93
+ service = get_service(user)
94
+ except HTTPError as e:
95
+ return HttpResponseRedirect(reverse('social:begin', kwargs={'backend': "google-oauth2"}))
96
+
97
+ if not organization.google_drive_parent:
98
+ if user == organization.owner:
99
+ create_root(user)
100
+ else:
101
+ messages.error(request,
102
+ "There's no root Google Drive directory and you're not the owner. Contact your organization owner to set up Google Drive")
103
+ else:
104
+ if user == organization.owner:
105
+ try:
106
+ service.files().get(fileId=organization.google_drive_parent).execute()
107
+ except HttpError:
108
+ create_root(user)
109
+
110
+ try:
111
+ part = Part.objects.get(id=part_id)
112
+ except ObjectDoesNotExist:
113
+ messages.error(request, "Part object does not exist.")
114
+ return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
115
+
116
+ if part.google_drive_parent:
117
+ try:
118
+ service.files().get(fileId=part.google_drive_parent).execute()
119
+ except HttpError:
120
+ if user == organization.owner:
121
+ # if they aren't the owner, let's just try to go to the folder...
122
+ try:
123
+ create_part_folder(user, part)
124
+ except HttpError as e:
125
+ messages.error(request, "Error: {}".format(e))
126
+ return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
127
+ # TODO: Check if the folder name exists already before creating it ?
128
+ else:
129
+ try:
130
+ create_part_folder(user, part)
131
+ except HttpError as e:
132
+ for detail in e.error_details:
133
+ msg = detail['message'] if 'message' in detail else 'Unknown error'
134
+ if 'reason' in detail and detail['reason'] == 'notFound':
135
+ msg += '<br>You may need to ask your organization owner to share the organization folder with you.'
136
+ messages.error(request, "Error: {}".format(msg))
137
+
138
+ return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
139
+
140
+ return HttpResponseRedirect('https://drive.google.com/drive/folders/{}'.format(part.google_drive_parent))
141
+
142
+
143
+ @login_required
144
+ @google_authenticated
145
+ def update_folder_name(request, part_id):
146
+ user = request.user
147
+ organization = user.bom_profile().organization
148
+ try:
149
+ service = get_service(user)
150
+ except HTTPError as e:
151
+ return HttpResponseRedirect(reverse('social:begin', kwargs={'backend': "google-oauth2"}))
152
+
153
+ if not organization.google_drive_parent:
154
+ create_root(user)
155
+
156
+ try:
157
+ part = Part.objects.get(id=part_id)
158
+ except ObjectDoesNotExist:
159
+ messages.error(request, "Part object does not exist.")
160
+ return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
161
+
162
+ if part.google_drive_parent:
163
+ new_filename = part.full_part_number() + ' ' + part.latest().description
164
+ service.files().update(fileId=part.google_drive_parent, body={'name': new_filename}).execute()
165
+ # TODO: Finish this... should update filename on
166
+ return
@@ -0,0 +1,132 @@
1
+ from moneyed import Money
2
+
3
+ from bom.utils import parse_number
4
+ from djmoney.contrib.exchange.models import convert_money
5
+ from .base_api import BaseApi, BaseApiError
6
+ from ..models import SellerPart, Seller
7
+ import json
8
+
9
+
10
+ class MouserApi(BaseApi):
11
+ def __init__(self, *args, **kwargs):
12
+ api_settings_key = 'mouser_api_key'
13
+ root_url='https://api.mouser.com/api/v1'
14
+ api_key_query = 'apiKey'
15
+ super().__init__(api_settings_key, root_url, api_key_query=api_key_query)
16
+
17
+ @staticmethod
18
+ def parse_and_check_for_errors(content):
19
+ data = json.loads(content)
20
+ errors = data['Errors']
21
+ if len(errors) > 0:
22
+ raise BaseApiError("Error(s): {}".format(errors))
23
+ return data
24
+
25
+ def search_keyword(self, keyword):
26
+ content = self.request('/search/keyword', data={
27
+ "SearchByKeywordRequest": {
28
+ "keyword": keyword,
29
+ "records": 0,
30
+ "startingRecord": 0,
31
+ "searchOptions": "",
32
+ "searchWithYourSignUpLanguage": ""
33
+ }
34
+ })
35
+ data = self.parse_and_check_for_errors(content)
36
+ return data["SearchResults"]
37
+
38
+ def get_manufacturer_list(self):
39
+ content = self.request('/search/manufacturerlist')
40
+ data = self.parse_and_check_for_errors(content)
41
+ return data["MouserManufacturerList"]
42
+
43
+ def search_part(self, part_number):
44
+ content = self.request('/search/partnumber', data={
45
+ "SearchByPartRequest": {
46
+ "mouserPartNumber": part_number,
47
+ "partSearchOptions": "",
48
+ }
49
+ })
50
+ data = self.parse_and_check_for_errors(content)
51
+ return data["SearchResults"]
52
+
53
+ def search_part_and_manufacturer(self, part_number, manufacturer_id):
54
+ content = self.request('/search/partnumberandmanufacturer', data={
55
+ "SearchByPartMfrRequest": {
56
+ "manufacturerId": manufacturer_id,
57
+ "mouserPartNumber": part_number,
58
+ "partSearchOptions": "",
59
+ }
60
+ })
61
+ data = self.parse_and_check_for_errors(content)
62
+ return data["SearchResults"]
63
+
64
+
65
+ class Mouser:
66
+ def __init__(self):
67
+ self.api = MouserApi()
68
+
69
+ def search_and_match(self, manufacturer_part, quantity=1, currency=None):
70
+ manufacturer = manufacturer_part.manufacturer
71
+ manufacturer_part_number = manufacturer_part.manufacturer_part_number
72
+ if manufacturer:
73
+ manufacturer_list = self.api.get_manufacturer_list()
74
+ # TODO: possibly get manufacturer id from manufacturer list, do a fuzzy lookup using manufacturer name
75
+ # to reduce results
76
+ mfg_id = manufacturer_list[manufacturer.name] if manufacturer.name in manufacturer_list else None
77
+ if mfg_id:
78
+ results = self.api.search_part_and_manufacturer(part_number=manufacturer_part_number, manufacturer_id=mfg_id)
79
+ else:
80
+ results = self.api.search_part(part_number=manufacturer_part_number)
81
+ else:
82
+ results = self.api.search_part(part_number=manufacturer_part_number)
83
+
84
+ mouser_parts = []
85
+ optimal_part = None
86
+ seller_parts = []
87
+ for part in results['Parts']:
88
+ seller = Seller(name='Mouser')
89
+ try:
90
+ quantity_available = [int(s) for s in part['Availability'].split() if s.isdigit()][0]
91
+ mouser_part = {
92
+ 'part_number': part['ManufacturerPartNumber'],
93
+ 'manufacturer': part['Manufacturer'],
94
+ 'description': part['Description'],
95
+ 'data_sheet': part['DataSheetUrl'],
96
+ 'stock': part['Availability'],
97
+ 'stock_parsed': quantity_available,
98
+ 'lead_time': part['LeadTime'],
99
+ 'seller_parts': [],
100
+ 'product_detail_url': part['ProductDetailUrl'],
101
+ }
102
+
103
+ lead_time_days = [int(s) for s in part['LeadTime'].split() if s.isdigit()][0] # TODO: Make sure it's actually days
104
+ for pb in part['PriceBreaks']:
105
+ moq = int(pb['Quantity'])
106
+ unit_price_raw = parse_number(pb['Price'])
107
+ unit_currency = pb['Currency']
108
+ unit_cost = Money(unit_price_raw, unit_currency)
109
+ if currency:
110
+ unit_cost = convert_money(unit_cost, currency)
111
+ seller_part = SellerPart(
112
+ seller=seller,
113
+ seller_part_number=part['MouserPartNumber'],
114
+ manufacturer_part=manufacturer_part,
115
+ minimum_order_quantity=moq,
116
+ minimum_pack_quantity=1,
117
+ data_source='Mouser',
118
+ unit_cost=unit_cost,
119
+ lead_time_days=lead_time_days,
120
+ nre_cost=Money(0, currency),
121
+ ncnr=True)
122
+ mouser_part['seller_parts'].append(seller_part.as_dict())
123
+ seller_parts.append(seller_part)
124
+ mouser_parts.append(mouser_part)
125
+ except (KeyError, AttributeError, IndexError):
126
+ continue
127
+ local_seller_parts = list(manufacturer_part.seller_parts())
128
+ seller_parts.extend(local_seller_parts)
129
+ return {
130
+ 'mouser_parts': mouser_parts,
131
+ 'optimal_seller_part': SellerPart.optimal(seller_parts, quantity),
132
+ }
@@ -0,0 +1,24 @@
1
+ from django.test import TestCase
2
+ from .mouser import MouserApi
3
+ from unittest import skip
4
+
5
+
6
+ class TestMouser(TestCase):
7
+ def setUp(self):
8
+ self.api = MouserApi()
9
+
10
+ @skip
11
+ def test_search_keyword(self):
12
+ search = self.api.search_keyword(keyword='LSM6DSL')
13
+ self.assertGreaterEqual(search['NumberOfResult'], 1)
14
+
15
+ @skip
16
+ def test_get_manufacturer_list(self):
17
+ manufacturers = self.api.get_manufacturer_list()
18
+ self.assertGreaterEqual(manufacturers['Count'], 1)
19
+
20
+ @skip
21
+ def search_part_and_manufacturer(self):
22
+ manufacturers = self.api.get_manufacturer_list()
23
+ self.assertGreaterEqual(manufacturers['Count'], 1)
24
+
bom/urls.py ADDED
@@ -0,0 +1,100 @@
1
+ from django.conf import settings
2
+ from django.contrib import admin
3
+ from django.contrib.auth import views as auth_views
4
+ from django.urls import include, path
5
+ from django.views.generic import TemplateView
6
+
7
+ from bom.third_party_apis import google_drive
8
+ from bom.views import json_views, views
9
+
10
+ BOM_CONFIG = getattr(settings, 'BOM_CONFIG', {})
11
+ standalone_mode = BOM_CONFIG.get('standalone_mode', True)
12
+
13
+ bom_patterns = [
14
+ # BOM urls
15
+ path('', views.home, name='home'),
16
+ # path('bom/', views.home, name='home'),
17
+ path('create-organization/', views.organization_create, name='organization-create'),
18
+ path('help/', views.Help.as_view(), name=views.Help.name),
19
+ path('search-help/', views.search_help, name='search-help'),
20
+ path('bom-signup/', views.bom_signup, name='bom-signup'),
21
+ path('settings/', views.bom_settings, name='settings'),
22
+ path('settings/<str:tab_anchor>/', views.bom_settings, name='settings'),
23
+ path('manufacturers/', views.manufacturers, name='manufacturers'),
24
+ path('manufacturer/<int:manufacturer_id>/', views.manufacturer_info, name='manufacturer-info'),
25
+ path('manufacturer/<int:manufacturer_id>/edit/', views.manufacturer_edit, name='manufacturer-edit'),
26
+ path('manufacturer/<int:manufacturer_id>/delete/', views.manufacturer_delete, name='manufacturer-delete'),
27
+ path('sellers/', views.sellers, name='sellers'),
28
+ path('seller/<int:seller_id>/', views.seller_info, name='seller-info'),
29
+ path('seller/<int:seller_id>/edit/', views.seller_edit, name='seller-edit'),
30
+ path('seller/<int:seller_id>/delete/', views.seller_delete, name='seller-delete'),
31
+ path('export/', views.export_part_list, name='export-part-list'),
32
+ path('user-meta/<int:user_meta_id>/edit/', views.user_meta_edit, name='user-meta-edit'),
33
+ path('part-class/<int:part_class_id>/edit/', views.part_class_edit, name='part-class-edit'),
34
+ path('property-definition/add/', views.property_definition_edit, name='property-definition-add'),
35
+ path('property-definition/<int:property_definition_id>/edit/', views.property_definition_edit, name='property-definition-edit'),
36
+ path('property-definition/<int:property_definition_id>/delete/', views.property_definition_delete, name='property-definition-delete'),
37
+ path('quantity-of-measure/add/', views.quantity_of_measure_edit, name='quantity-of-measure-add'),
38
+ path('quantity-of-measure/<int:quantity_of_measure_id>/edit/', views.quantity_of_measure_edit, name='quantity-of-measure-edit'),
39
+ path('quantity-of-measure/<int:quantity_of_measure_id>/delete/', views.quantity_of_measure_delete, name='quantity-of-measure-delete'),
40
+ path('create-part/', views.create_part, name='create-part'),
41
+ path('upload-parts/', views.upload_parts, name='upload-parts'),
42
+ path('upload-parts-help/', views.upload_parts_help, name='upload-parts-help'),
43
+ path('upload-bom/', views.upload_bom, name='upload-bom'),
44
+ path('part/<int:part_id>/', views.part_info, name='part-info'),
45
+ path('part/<int:part_id>/export/', views.part_export_bom, name='part-export-bom'),
46
+ path('part/<int:part_id>/export-sourcing/', views.part_export_bom, name='part-export-bom-sourcing', kwargs={'sourcing': True}),
47
+ path('part/<int:part_id>/export-sourcing-detailed/', views.part_export_bom, name='part-export-bom-sourcing-detailed', kwargs={'sourcing_detailed': True}),
48
+ path('part/<int:part_id>/upload/', views.part_upload_bom, name='part-upload-bom'),
49
+ path('part/<int:part_id>/edit/', views.part_edit, name='part-edit'),
50
+ path('part/<int:part_id>/delete/', views.part_delete, name='part-delete'),
51
+ path('part/<int:part_id>/add-manufacturer-part/', views.add_manufacturer_part, name='part-add-manufacturer-part'),
52
+ path('part/<int:part_id>/rev/new/', views.part_revision_new, name='part-revision-new'),
53
+ path('part/<int:part_id>/rev/<int:part_revision_id>/', views.part_info, name='part-info-history'),
54
+ path('part/<int:part_id>/rev/<int:part_revision_id>/edit/', views.part_revision_edit, name='part-revision-edit'),
55
+ path('part/<int:part_id>/rev/<int:part_revision_id>/delete/', views.part_revision_delete, name='part-revision-delete'),
56
+ path('part/<int:part_id>/rev/<int:part_revision_id>/release/', views.part_revision_release, name='part-revision-release'),
57
+ path('part/<int:part_id>/rev/<int:part_revision_id>/revert/', views.part_revision_revert, name='part-revision-revert'),
58
+ path('part/<int:part_id>/rev/<int:part_revision_id>/remove-all-subparts/', views.remove_all_subparts, name='part-remove-all-subparts'),
59
+ path('part/<int:part_id>/rev/<int:part_revision_id>/edit-subpart/<int:subpart_id>', views.edit_subpart, name='part-edit-subpart'),
60
+ path('part/<int:part_id>/rev/<int:part_revision_id>/remove-subpart/<int:subpart_id>/', views.remove_subpart, name='part-remove-subpart'),
61
+ path('part/<int:part_id>/rev/<int:part_revision_id>/manage-bom/', views.manage_bom, name='part-manage-bom'),
62
+ path('part/<int:part_id>/rev/<int:part_revision_id>/add-subpart/', views.add_subpart, name='part-add-subpart'),
63
+
64
+ path('part-rev/<int:part_revision_id>/export/', views.part_export_bom, name='part-revision-export-bom'),
65
+ path('part-rev/<int:part_revision_id>/export-sourcing/', views.part_export_bom, name='part-revision-export-bom-sourcing', kwargs={'sourcing': True}),
66
+ path('part-rev/<int:part_revision_id>/export-sourcing-detailed/', views.part_export_bom, name='part-revision-export-bom-sourcing-detailed', kwargs={'sourcing_detailed': True}),
67
+ path('part-rev/<int:part_revision_id>/export-flat/', views.part_export_bom, name='part-revision-export-bom-flat', kwargs={'flat': True}),
68
+ path('part-rev/<int:part_revision_id>/export-flat-sourcing/', views.part_export_bom, name='part-revision-export-bom-flat-sourcing', kwargs={'flat': True, 'sourcing': True}),
69
+ path('part-rev/<int:part_revision_id>/export-flat-sourcing-detailed/', views.part_export_bom, name='part-revision-export-bom-flat-sourcing-detailed', kwargs={'flat': True, 'sourcing_detailed': True}),
70
+
71
+ path('sellerpart/<int:sellerpart_id>/edit/', views.sellerpart_edit, name='sellerpart-edit'),
72
+ path('sellerpart/<int:sellerpart_id>/delete/', views.sellerpart_delete, name='sellerpart-delete'),
73
+ path('manufacturer-part/<int:manufacturer_part_id>/add-sellerpart/', views.add_sellerpart, name='manufacturer-part-add-sellerpart'),
74
+ path('manufacturer-part/<int:manufacturer_part_id>/edit', views.manufacturer_part_edit, name='manufacturer-part-edit'),
75
+ path('manufacturer-part/<int:manufacturer_part_id>/delete', views.manufacturer_part_delete, name='manufacturer-part-delete'),
76
+ ]
77
+
78
+ google_drive_patterns = [
79
+ path('folder/<int:part_id>/', google_drive.get_or_create_and_open_folder, name='add-folder'),
80
+ ]
81
+
82
+ json_patterns = [
83
+ path('mouser-part-match-bom/<int:part_revision_id>/', json_views.MouserPartMatchBOM.as_view(), name='mouser-part-match-bom')
84
+ ]
85
+
86
+ urlpatterns = [
87
+ path('', include((bom_patterns, 'bom'))),
88
+ path('', include('social_django.urls', namespace='social')),
89
+ path('google-drive/', include((google_drive_patterns, 'google-drive'))),
90
+ path('json/', include((json_patterns, 'json'))),
91
+ ]
92
+
93
+ if standalone_mode:
94
+ urlpatterns += [
95
+ path('admin/', admin.site.urls),
96
+ path('signup/', views.signup, name='signup'),
97
+ path('login/', auth_views.LoginView.as_view(redirect_authenticated_user=True), name='login'),
98
+ path('logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
99
+ path('account/delete/', TemplateView.as_view(template_name='bom/account-delete.html'), name='account-delete'),
100
+ ]