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.
- bom/__init__.py +1 -0
- bom/admin.py +207 -0
- bom/apps.py +8 -0
- bom/auth_backends.py +47 -0
- bom/base_classes.py +31 -0
- bom/constants.py +217 -0
- bom/context_processors.py +9 -0
- bom/csv_headers.py +252 -0
- bom/decorators.py +32 -0
- bom/form_fields.py +59 -0
- bom/forms.py +1328 -0
- bom/helpers.py +367 -0
- bom/local_settings.py +35 -0
- bom/migrations/0001_initial.py +135 -0
- bom/migrations/0002_auto_20180908_2151.py +24 -0
- bom/migrations/0003_sellerpart_data_source.py +18 -0
- bom/migrations/0004_auto_20180911_0011.py +18 -0
- bom/migrations/0005_auto_20181007_1934.py +56 -0
- bom/migrations/0006_auto_20181007_1949.py +41 -0
- bom/migrations/0007_auto_20181009_0256.py +19 -0
- bom/migrations/0008_auto_20181030_0427.py +19 -0
- bom/migrations/0009_subpart_reference.py +18 -0
- bom/migrations/0010_auto_20181202_0733.py +23 -0
- bom/migrations/0011_auto_20181202_2113.py +22 -0
- bom/migrations/0012_partchangehistory.py +30 -0
- bom/migrations/0013_auto_20190222_1631.py +19 -0
- bom/migrations/0014_auto_20190223_2353.py +18 -0
- bom/migrations/0015_auto_20190303_1915.py +136 -0
- bom/migrations/0016_auto_20190405_2308.py +58 -0
- bom/migrations/0017_auto_20190616_1912.py +19 -0
- bom/migrations/0018_auto_20190616_2143.py +24 -0
- bom/migrations/0019_auto_20190624_1246.py +45 -0
- bom/migrations/0020_auto_20190627_0207.py +38 -0
- bom/migrations/0021_auto_20190627_0428.py +23 -0
- bom/migrations/0022_auto_20190811_2140.py +35 -0
- bom/migrations/0023_auto_20191205_2351.py +255 -0
- bom/migrations/0024_auto_20191214_1342.py +89 -0
- bom/migrations/0025_auto_20191221_1907.py +38 -0
- bom/migrations/0026_auto_20191222_2258.py +22 -0
- bom/migrations/0027_auto_20191222_2347.py +17 -0
- bom/migrations/0028_partrevision_displayable_synopsis.py +74 -0
- bom/migrations/0029_auto_20191231_1630.py +23 -0
- bom/migrations/0030_auto_20200101_2253.py +22 -0
- bom/migrations/0031_auto_20200104_1352.py +38 -0
- bom/migrations/0032_auto_20200126_1806.py +27 -0
- bom/migrations/0033_auto_20200203_0618.py +29 -0
- bom/migrations/0034_auto_20200222_0359.py +30 -0
- bom/migrations/0035_auto_20200303_0111.py +34 -0
- bom/migrations/0036_auto_20200303_0538.py +17 -0
- bom/migrations/0037_auto_20200405_1642.py +44 -0
- bom/migrations/0038_auto_20200422_0504.py +19 -0
- bom/migrations/0039_auto_20200929_2315.py +41 -0
- bom/migrations/0040_alter_organization_currency.py +19 -0
- bom/migrations/0041_organization_subscription_quantity.py +18 -0
- bom/migrations/0042_auto_20210720_2137.py +23 -0
- bom/migrations/0043_auto_20211123_0157.py +24 -0
- bom/migrations/0044_auto_20220831_1241.py +23 -0
- bom/migrations/0045_sellerpart_link.py +18 -0
- bom/migrations/0046_alter_sellerpart_unique_together.py +17 -0
- bom/migrations/0047_sellerpart_seller_part_number.py +18 -0
- bom/migrations/0048_rename_part_organization_number_class_bom_part_organiz_b333d6_idx_and_more.py +1017 -0
- bom/migrations/0049_alter_assembly_id_alter_assemblysubparts_id_and_more.py +99 -0
- bom/migrations/0050_alter_organization_options.py +17 -0
- bom/migrations/0051_alter_manufacturer_organization_and_more.py +41 -0
- bom/migrations/0052_remove_partrevision_attribute_and_more.py +584 -0
- bom/migrations/__init__.py +0 -0
- bom/models.py +886 -0
- bom/part_bom.py +192 -0
- bom/settings.py +262 -0
- bom/static/bom/css/dashboard.css +17 -0
- bom/static/bom/css/jquery.treetable.css +28 -0
- bom/static/bom/css/materialize.min.css +13 -0
- bom/static/bom/css/part-info.css +15 -0
- bom/static/bom/css/style.css +482 -0
- bom/static/bom/css/tablesorter-theme.materialize.css +176 -0
- bom/static/bom/css/treetable-theme.css +42 -0
- bom/static/bom/doc/sample_part_classes.csv +38 -0
- bom/static/bom/doc/test_bom.csv +6 -0
- bom/static/bom/doc/test_bom_5_intelligent.csv +4 -0
- bom/static/bom/doc/test_full_bom.csv +37 -0
- bom/static/bom/doc/test_new_parts.csv +5 -0
- bom/static/bom/doc/test_new_parts_5_intelligent.csv +5 -0
- bom/static/bom/img/_ionicons_svg_md-arrow-dropdown.svg +1 -0
- bom/static/bom/img/_ionicons_svg_md-arrow-dropright.svg +1 -0
- bom/static/bom/img/favicon.ico +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_dark_disabled_web.png +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_dark_focus_web.png +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_dark_normal_web.png +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_dark_pressed_web.png +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_light_disabled_web.png +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_light_focus_web.png +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_light_normal_web.png +0 -0
- bom/static/bom/img/google/web/1x/btn_google_signin_light_pressed_web.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_dark_disabled_web@2x.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_dark_focus_web@2x.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_dark_normal_web@2x.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_dark_pressed_web@2x.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_light_disabled_web@2x.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_light_focus_web@2x.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_light_normal_web@2x.png +0 -0
- bom/static/bom/img/google/web/2x/btn_google_signin_light_pressed_web@2x.png +0 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_disabled_ios.eps +814 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_disabled_ios.svg +24 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_focus_ios.eps +1866 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_focus_ios.svg +51 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_normal_ios.eps +1031 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_normal_ios.svg +50 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_pressed_ios.eps +1031 -0
- bom/static/bom/img/google/web/vector/btn_google_dark_pressed_ios.svg +50 -0
- bom/static/bom/img/google/web/vector/btn_google_light_disabled_ios.eps +814 -0
- bom/static/bom/img/google/web/vector/btn_google_light_disabled_ios.svg +24 -0
- bom/static/bom/img/google/web/vector/btn_google_light_focus_ios.eps +1837 -0
- bom/static/bom/img/google/web/vector/btn_google_light_focus_ios.svg +44 -0
- bom/static/bom/img/google/web/vector/btn_google_light_normal_ios.eps +1002 -0
- bom/static/bom/img/google/web/vector/btn_google_light_normal_ios.svg +43 -0
- bom/static/bom/img/google/web/vector/btn_google_light_pressed_ios.eps +1002 -0
- bom/static/bom/img/google/web/vector/btn_google_light_pressed_ios.svg +43 -0
- bom/static/bom/img/google_drive_logo.svg +1 -0
- bom/static/bom/img/indabom.png +0 -0
- bom/static/bom/img/mouser.png +0 -0
- bom/static/bom/img/octopart_blue.svg +19 -0
- bom/static/bom/js/formset-handler.js +65 -0
- bom/static/bom/js/jquery-3.4.1.min.js +2 -0
- bom/static/bom/js/jquery.ba-floatingscrollbar.min.js +10 -0
- bom/static/bom/js/jquery.treetable.js +629 -0
- bom/static/bom/js/materialize.min.js +6 -0
- bom/templates/bom/account-delete.html +23 -0
- bom/templates/bom/add-manufacturer-part.html +66 -0
- bom/templates/bom/add-sellerpart.html +93 -0
- bom/templates/bom/base-menu.html +16 -0
- bom/templates/bom/base.html +129 -0
- bom/templates/bom/bom-action-btn.html +23 -0
- bom/templates/bom/bom-action-table.html +57 -0
- bom/templates/bom/bom-base-menu.html +6 -0
- bom/templates/bom/bom-base.html +24 -0
- bom/templates/bom/bom-form-modal.html +36 -0
- bom/templates/bom/bom-form.html +30 -0
- bom/templates/bom/bom-modal-add-users.html +49 -0
- bom/templates/bom/bom-signup.html +12 -0
- bom/templates/bom/components/bom-flat.html +131 -0
- bom/templates/bom/components/bom-indented.html +237 -0
- bom/templates/bom/components/manufacturer-part-list.html +270 -0
- bom/templates/bom/components/seller-part-list.html +62 -0
- bom/templates/bom/create-part.html +65 -0
- bom/templates/bom/dashboard-menu.html +15 -0
- bom/templates/bom/dashboard.html +303 -0
- bom/templates/bom/edit-manufacturer-part.html +72 -0
- bom/templates/bom/edit-part-class.html +120 -0
- bom/templates/bom/edit-part.html +67 -0
- bom/templates/bom/edit-quantity-of-measure.html +119 -0
- bom/templates/bom/edit-user-meta.html +70 -0
- bom/templates/bom/help.html +1356 -0
- bom/templates/bom/manufacturer-info.html +82 -0
- bom/templates/bom/manufacturers.html +97 -0
- bom/templates/bom/nothing-to-see.html +15 -0
- bom/templates/bom/organization-create.html +135 -0
- bom/templates/bom/part-info.html +448 -0
- bom/templates/bom/part-revision-display.html +50 -0
- bom/templates/bom/part-revision-edit.html +39 -0
- bom/templates/bom/part-revision-manage-bom.html +115 -0
- bom/templates/bom/part-revision-new.html +57 -0
- bom/templates/bom/part-revision-release.html +41 -0
- bom/templates/bom/search-help.html +101 -0
- bom/templates/bom/seller-info.html +82 -0
- bom/templates/bom/sellers.html +97 -0
- bom/templates/bom/settings.html +734 -0
- bom/templates/bom/signup.html +28 -0
- bom/templates/bom/subscription_panel.html +16 -0
- bom/templates/bom/table_of_contents.html +47 -0
- bom/templates/bom/upload-bom.html +111 -0
- bom/templates/bom/upload-parts-help.html +103 -0
- bom/templates/bom/upload-parts.html +50 -0
- bom/templates/registration/login.html +39 -0
- bom/tests.py +1592 -0
- bom/third_party_apis/__init__.py +0 -0
- bom/third_party_apis/base_api.py +51 -0
- bom/third_party_apis/google_drive.py +166 -0
- bom/third_party_apis/mouser.py +132 -0
- bom/third_party_apis/test_apis.py +24 -0
- bom/urls.py +100 -0
- bom/utils.py +228 -0
- bom/validators.py +23 -0
- bom/views/__init__.py +0 -0
- bom/views/json_views.py +55 -0
- bom/views/views.py +1773 -0
- bom/wsgi.py +16 -0
- django_bom-1.262.dist-info/METADATA +206 -0
- django_bom-1.262.dist-info/RECORD +191 -0
- django_bom-1.262.dist-info/WHEEL +5 -0
- django_bom-1.262.dist-info/licenses/LICENSE +674 -0
- 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
|
+
]
|