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
bom/views/views.py
ADDED
|
@@ -0,0 +1,1773 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import logging
|
|
3
|
+
import operator
|
|
4
|
+
from functools import reduce
|
|
5
|
+
from json import dumps
|
|
6
|
+
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from django.contrib import messages
|
|
9
|
+
from django.contrib.auth import login
|
|
10
|
+
from django.contrib.auth.decorators import login_required
|
|
11
|
+
from django.core.cache import cache
|
|
12
|
+
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
|
13
|
+
from django.db import IntegrityError
|
|
14
|
+
from django.db.models import Count, ProtectedError, Q, Subquery, prefetch_related_objects
|
|
15
|
+
from django.db.models.aggregates import Max
|
|
16
|
+
from django.http import HttpResponse, HttpResponseRedirect
|
|
17
|
+
from django.shortcuts import get_object_or_404
|
|
18
|
+
from django.template.response import TemplateResponse
|
|
19
|
+
from django.urls import reverse
|
|
20
|
+
from django.utils.encoding import smart_str
|
|
21
|
+
from django.utils.text import smart_split
|
|
22
|
+
from django.views.generic.base import TemplateView
|
|
23
|
+
from social_django.models import UserSocialAuth
|
|
24
|
+
|
|
25
|
+
import bom.constants as constants
|
|
26
|
+
from bom.csv_headers import (
|
|
27
|
+
BOMFlatCSVHeaders,
|
|
28
|
+
BOMIndentedCSVHeaders,
|
|
29
|
+
ManufacturerPartCSVHeaders,
|
|
30
|
+
PartClassesCSVHeaders,
|
|
31
|
+
SellerPartCSVHeaders,
|
|
32
|
+
)
|
|
33
|
+
from bom.decorators import organization_admin
|
|
34
|
+
from bom.forms import (
|
|
35
|
+
AddSubpartForm,
|
|
36
|
+
BOMCSVForm,
|
|
37
|
+
FileForm,
|
|
38
|
+
ManufacturerForm,
|
|
39
|
+
ManufacturerPartForm,
|
|
40
|
+
OrganizationCreateForm,
|
|
41
|
+
OrganizationFormEditSettings,
|
|
42
|
+
OrganizationNumberLenForm,
|
|
43
|
+
PartClassCSVForm,
|
|
44
|
+
PartClassForm,
|
|
45
|
+
PartClassSelectionForm,
|
|
46
|
+
PartCSVForm,
|
|
47
|
+
PartInfoForm,
|
|
48
|
+
PartRevisionForm,
|
|
49
|
+
PartRevisionNewForm,
|
|
50
|
+
PartRevisionPropertyDefinitionForm,
|
|
51
|
+
PartRevisionPropertyDefinitionFormSet,
|
|
52
|
+
QuantityOfMeasureForm,
|
|
53
|
+
Seller,
|
|
54
|
+
SellerForm,
|
|
55
|
+
SellerPartForm,
|
|
56
|
+
SubpartForm,
|
|
57
|
+
UnitDefinitionFormSet,
|
|
58
|
+
UploadBOMForm,
|
|
59
|
+
UserAddForm,
|
|
60
|
+
UserCreateForm,
|
|
61
|
+
UserForm,
|
|
62
|
+
UserMetaForm,
|
|
63
|
+
part_form_from_organization,
|
|
64
|
+
)
|
|
65
|
+
from bom.models import (
|
|
66
|
+
Assembly,
|
|
67
|
+
AssemblySubparts,
|
|
68
|
+
Manufacturer,
|
|
69
|
+
ManufacturerPart,
|
|
70
|
+
Part,
|
|
71
|
+
PartClass,
|
|
72
|
+
PartRevision,
|
|
73
|
+
PartRevisionPropertyDefinition,
|
|
74
|
+
QuantityOfMeasure,
|
|
75
|
+
SellerPart,
|
|
76
|
+
Subpart,
|
|
77
|
+
UnitDefinition,
|
|
78
|
+
User,
|
|
79
|
+
get_user_meta_model
|
|
80
|
+
)
|
|
81
|
+
from bom.utils import check_references_for_duplicates, listify_string, prep_for_sorting_nicely
|
|
82
|
+
|
|
83
|
+
logger = logging.getLogger(__name__)
|
|
84
|
+
UserMeta = get_user_meta_model()
|
|
85
|
+
BOM_LOGIN_URL = getattr(settings, "BOM_LOGIN_URL", None) or settings.LOGIN_URL
|
|
86
|
+
|
|
87
|
+
def form_error_messages(form_errors) -> [str]:
|
|
88
|
+
error_messages = []
|
|
89
|
+
for k, errors in form_errors.as_data().items():
|
|
90
|
+
for error_message in errors:
|
|
91
|
+
error_messages.append(str(error_message.message))
|
|
92
|
+
|
|
93
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
94
|
+
def home(request):
|
|
95
|
+
profile = request.user.bom_profile()
|
|
96
|
+
organization = profile.organization
|
|
97
|
+
if not organization:
|
|
98
|
+
return HttpResponseRedirect(reverse('bom:organization-create'))
|
|
99
|
+
|
|
100
|
+
query = request.GET.get('q', '')
|
|
101
|
+
title = f'{organization.name}\'s'
|
|
102
|
+
|
|
103
|
+
# Note that posting a PartClass selection does not include a named parameter in
|
|
104
|
+
# the POST, so this case is the de facto "else" clause.
|
|
105
|
+
part_class_selection_form = PartClassSelectionForm(organization=organization)
|
|
106
|
+
|
|
107
|
+
if request.method == 'GET':
|
|
108
|
+
part_class_selection_form = PartClassSelectionForm(request.GET, organization=organization)
|
|
109
|
+
elif request.method == 'POST':
|
|
110
|
+
if 'actions' in request.POST and 'part-action' in request.POST:
|
|
111
|
+
action = request.POST.get('part-action')
|
|
112
|
+
if action == 'Delete':
|
|
113
|
+
part_ids = [part_id for part_id in request.POST.getlist('actions') if part_id.isdigit()]
|
|
114
|
+
for part_id in part_ids:
|
|
115
|
+
try:
|
|
116
|
+
part = Part.objects.get(id=part_id, organization=organization)
|
|
117
|
+
part_number = part.full_part_number()
|
|
118
|
+
part.delete()
|
|
119
|
+
messages.success(request, f"Deleted part {part_number}")
|
|
120
|
+
except Part.DoesNotExist:
|
|
121
|
+
messages.error(request, "Can't delete part. No part found with given id {}.".format(part_id))
|
|
122
|
+
|
|
123
|
+
if part_class_selection_form.is_valid():
|
|
124
|
+
part_class = part_class_selection_form.cleaned_data['part_class']
|
|
125
|
+
else:
|
|
126
|
+
part_class = None
|
|
127
|
+
|
|
128
|
+
if part_class or query:
|
|
129
|
+
title += f' - Search Results'
|
|
130
|
+
else:
|
|
131
|
+
title += f' Part List'
|
|
132
|
+
|
|
133
|
+
if part_class:
|
|
134
|
+
parts = Part.objects.filter(Q(organization=organization) & Q(number_class__code=part_class.code))
|
|
135
|
+
else:
|
|
136
|
+
parts = Part.objects.filter(organization=organization)
|
|
137
|
+
|
|
138
|
+
part_ids = list(parts.values_list('id', flat=True))
|
|
139
|
+
|
|
140
|
+
part_revs = PartRevision.objects\
|
|
141
|
+
.filter(id__in=Subquery(
|
|
142
|
+
PartRevision.objects.filter(
|
|
143
|
+
part_id__in=part_ids
|
|
144
|
+
).annotate(max_id=Max('id')).values("id")
|
|
145
|
+
)).order_by(
|
|
146
|
+
"part__number_class__code",
|
|
147
|
+
"part__number_item",
|
|
148
|
+
"part__number_variation",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
autocomplete_dict = {}
|
|
152
|
+
enable_autocomplete = settings.BOM_CONFIG.get('admin_dashboard', {}).get('enable_autocomplete', False)
|
|
153
|
+
if enable_autocomplete:
|
|
154
|
+
prefetch_related_objects(part_revs, 'part')
|
|
155
|
+
manufacturer_parts = ManufacturerPart.objects.filter(part__in=parts)
|
|
156
|
+
|
|
157
|
+
for pr in part_revs:
|
|
158
|
+
autocomplete_dict.update({pr.searchable_synopsis.replace('"', ''): None})
|
|
159
|
+
autocomplete_dict.update({pr.part.full_part_number(): None})
|
|
160
|
+
|
|
161
|
+
for mpn in manufacturer_parts:
|
|
162
|
+
if mpn.manufacturer_part_number:
|
|
163
|
+
autocomplete_dict.update({mpn.manufacturer_part_number.replace('"', ''): None})
|
|
164
|
+
if mpn.manufacturer is not None and mpn.manufacturer.name:
|
|
165
|
+
autocomplete_dict.update({mpn.manufacturer.name.replace('"', ''): None})
|
|
166
|
+
|
|
167
|
+
autocomplete = dumps(autocomplete_dict)
|
|
168
|
+
|
|
169
|
+
if query:
|
|
170
|
+
query_stripped = query.strip()
|
|
171
|
+
|
|
172
|
+
# Parse terms separated by white space but keep together words inside of double quotes,
|
|
173
|
+
# for example
|
|
174
|
+
# "Big Company Inc."
|
|
175
|
+
# is parsed as 'Big Company Inc.' while
|
|
176
|
+
# Big Company Inc.
|
|
177
|
+
# is parsed as 'Big' 'Company' 'Inc.'
|
|
178
|
+
search_terms = query_stripped
|
|
179
|
+
search_terms = list(smart_split(search_terms))
|
|
180
|
+
search_terms = [search_term.replace('"', '') for search_term in search_terms]
|
|
181
|
+
noqoutes_query = query_stripped.replace('"', '')
|
|
182
|
+
|
|
183
|
+
number_class = None
|
|
184
|
+
number_item = None
|
|
185
|
+
number_variation = None
|
|
186
|
+
|
|
187
|
+
# Scan for search terms that might represent a complete or partial part number
|
|
188
|
+
if organization.number_scheme == constants.NUMBER_SCHEME_SEMI_INTELLIGENT:
|
|
189
|
+
for search_term in search_terms:
|
|
190
|
+
try:
|
|
191
|
+
(number_class, number_item, number_variation) = Part.parse_partial_part_number(search_term, organization, validate=False)
|
|
192
|
+
except AttributeError:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
# Query searchable_synopsis by OR'ing search terms
|
|
196
|
+
part_synopsis_ids = PartRevision.objects.filter(reduce(operator.or_, (Q(searchable_synopsis__icontains=term) for term in search_terms))).values_list("part", flat=True)
|
|
197
|
+
# Prepare Part.primary_manufacturer_part.manufacturer_part_number query by OR'ing search terms
|
|
198
|
+
q_primary_mpn = reduce(operator.or_, (Q(primary_manufacturer_part__manufacturer_part_number__icontains=term) for term in search_terms))
|
|
199
|
+
|
|
200
|
+
# Prepare Part.primary_manufacturer.part__manufacturer.name query by OR'ing search terms
|
|
201
|
+
q_primary_mfg = reduce(operator.or_, (Q(primary_manufacturer_part__manufacturer__name__icontains=term) for term in search_terms))
|
|
202
|
+
|
|
203
|
+
if number_class and number_item and number_variation:
|
|
204
|
+
parts = parts.filter(
|
|
205
|
+
Q(number_class__code=number_class, number_item=number_item, number_variation=number_variation) |
|
|
206
|
+
Q(id__in=part_synopsis_ids) |
|
|
207
|
+
q_primary_mpn |
|
|
208
|
+
q_primary_mfg)
|
|
209
|
+
elif number_class and number_item:
|
|
210
|
+
parts = parts.filter(
|
|
211
|
+
Q(number_class__code=number_class, number_item=number_item) |
|
|
212
|
+
Q(id__in=part_synopsis_ids) |
|
|
213
|
+
q_primary_mpn |
|
|
214
|
+
q_primary_mfg)
|
|
215
|
+
else:
|
|
216
|
+
parts = parts.filter(
|
|
217
|
+
Q(number_item__in=search_terms) |
|
|
218
|
+
Q(id__in=part_synopsis_ids) |
|
|
219
|
+
q_primary_mpn |
|
|
220
|
+
q_primary_mfg)
|
|
221
|
+
|
|
222
|
+
part_ids = list(parts.values_list('id', flat=True))
|
|
223
|
+
|
|
224
|
+
part_revs = PartRevision.objects \
|
|
225
|
+
.filter(id__in=Subquery(
|
|
226
|
+
PartRevision.objects.filter(
|
|
227
|
+
part_id__in=part_ids
|
|
228
|
+
).annotate(max_id=Max('id')).values("id")
|
|
229
|
+
)).order_by(
|
|
230
|
+
"part__number_class__code",
|
|
231
|
+
"part__number_item",
|
|
232
|
+
"part__number_variation"
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if 'download' in request.GET:
|
|
236
|
+
response = HttpResponse(content_type='text/csv')
|
|
237
|
+
response['Content-Disposition'] = 'attachment; filename="indabom_parts_search.csv"'
|
|
238
|
+
csv_headers = organization.part_list_csv_headers()
|
|
239
|
+
seller_csv_headers = SellerPartCSVHeaders()
|
|
240
|
+
writer = csv.DictWriter(response, fieldnames=csv_headers.get_default_all())
|
|
241
|
+
writer.writeheader()
|
|
242
|
+
for part_rev in part_revs:
|
|
243
|
+
if organization.number_scheme == constants.NUMBER_SCHEME_SEMI_INTELLIGENT:
|
|
244
|
+
row = {
|
|
245
|
+
csv_headers.get_default('part_number'): part_rev.part.full_part_number(),
|
|
246
|
+
csv_headers.get_default('part_category'): part_rev.part.number_class.name,
|
|
247
|
+
csv_headers.get_default('part_revision'): part_rev.revision,
|
|
248
|
+
csv_headers.get_default('part_manufacturer'): part_rev.part.primary_manufacturer_part.manufacturer.name if part_rev.part.primary_manufacturer_part is not None and
|
|
249
|
+
part_rev.part.primary_manufacturer_part.manufacturer is not None else '',
|
|
250
|
+
csv_headers.get_default('part_manufacturer_part_number'): part_rev.part.primary_manufacturer_part.manufacturer_part_number if part_rev.part.primary_manufacturer_part is not None else '',
|
|
251
|
+
}
|
|
252
|
+
for field_name in csv_headers.get_default_all():
|
|
253
|
+
if field_name not in csv_headers.get_defaults_list(['part_number', 'part_category', 'part_synopsis', 'part_revision', 'part_manufacturer', 'part_manufacturer_part_number', ]
|
|
254
|
+
+ seller_csv_headers.get_default_all()):
|
|
255
|
+
attr = part_rev.get_field_value(field_name)
|
|
256
|
+
row.update({csv_headers.get_default(field_name): attr if attr is not None else ''})
|
|
257
|
+
else:
|
|
258
|
+
row = {
|
|
259
|
+
csv_headers.get_default('part_number'): part_rev.part.full_part_number(),
|
|
260
|
+
csv_headers.get_default('part_revision'): part_rev.revision,
|
|
261
|
+
csv_headers.get_default('part_manufacturer'): part_rev.part.primary_manufacturer_part.manufacturer.name if part_rev.part.primary_manufacturer_part is not None and
|
|
262
|
+
part_rev.part.primary_manufacturer_part.manufacturer is not None else '',
|
|
263
|
+
csv_headers.get_default('part_manufacturer_part_number'): part_rev.part.primary_manufacturer_part.manufacturer_part_number if part_rev.part.primary_manufacturer_part is not None else '',
|
|
264
|
+
}
|
|
265
|
+
for field_name in csv_headers.get_default_all():
|
|
266
|
+
if field_name not in csv_headers.get_defaults_list(['part_number', 'part_synopsis', 'part_revision', 'part_manufacturer', 'part_manufacturer_part_number', ]
|
|
267
|
+
+ seller_csv_headers.get_default_all()):
|
|
268
|
+
attr = part_rev.get_field_value(field_name)
|
|
269
|
+
row.update({csv_headers.get_default(field_name): attr if attr is not None else ''})
|
|
270
|
+
|
|
271
|
+
sellerparts = part_rev.part.seller_parts()
|
|
272
|
+
if len(sellerparts) > 0:
|
|
273
|
+
for sellerpart in part_rev.part.seller_parts():
|
|
274
|
+
for field_name in seller_csv_headers.get_default_all():
|
|
275
|
+
attr = getattr(sellerpart, field_name)
|
|
276
|
+
row.update({csv_headers.get_default(field_name): attr if attr is not None else ''})
|
|
277
|
+
writer.writerow({k: smart_str(v) for k, v in row.items()})
|
|
278
|
+
else:
|
|
279
|
+
writer.writerow({k: smart_str(v) for k, v in row.items()})
|
|
280
|
+
return response
|
|
281
|
+
|
|
282
|
+
page_size = settings.BOM_CONFIG.get('admin_dashboard', {}).get('page_size', 25)
|
|
283
|
+
paginator = Paginator(part_revs, page_size)
|
|
284
|
+
|
|
285
|
+
page = request.GET.get('page')
|
|
286
|
+
try:
|
|
287
|
+
part_revs = paginator.page(page)
|
|
288
|
+
except PageNotAnInteger:
|
|
289
|
+
part_revs = paginator.page(1)
|
|
290
|
+
except EmptyPage:
|
|
291
|
+
part_revs = paginator.page(paginator.num_pages)
|
|
292
|
+
|
|
293
|
+
return TemplateResponse(request, 'bom/dashboard.html', locals())
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
297
|
+
def organization_create(request):
|
|
298
|
+
user = request.user
|
|
299
|
+
profile = user.bom_profile()
|
|
300
|
+
|
|
301
|
+
if user.first_name == '' and user.last_name == '':
|
|
302
|
+
org_name = user.username
|
|
303
|
+
else:
|
|
304
|
+
org_name = user.first_name + ' ' + user.last_name
|
|
305
|
+
|
|
306
|
+
form = OrganizationCreateForm(initial={'name': org_name, 'number_item_len': 4})
|
|
307
|
+
if request.method == 'POST':
|
|
308
|
+
form = OrganizationCreateForm(request.POST)
|
|
309
|
+
if form.is_valid():
|
|
310
|
+
organization = form.save(commit=False)
|
|
311
|
+
organization.owner = user
|
|
312
|
+
organization.subscription = constants.SUBSCRIPTION_TYPE_FREE
|
|
313
|
+
organization.save()
|
|
314
|
+
profile.organization = organization
|
|
315
|
+
profile.role = constants.ROLE_TYPE_ADMIN
|
|
316
|
+
profile.save()
|
|
317
|
+
return HttpResponseRedirect(reverse('bom:home'))
|
|
318
|
+
return TemplateResponse(request, 'bom/organization-create.html', locals())
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
322
|
+
def search_help(request):
|
|
323
|
+
return TemplateResponse(request, 'bom/search-help.html', locals())
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def signup(request):
|
|
327
|
+
name = 'signup'
|
|
328
|
+
|
|
329
|
+
if request.method == 'POST':
|
|
330
|
+
form = UserCreateForm(request.POST)
|
|
331
|
+
if form.is_valid():
|
|
332
|
+
new_user = form.save()
|
|
333
|
+
login(request, new_user, backend='django.contrib.auth.backends.ModelBackend')
|
|
334
|
+
return HttpResponseRedirect(reverse('bom:home'))
|
|
335
|
+
else:
|
|
336
|
+
form = UserCreateForm()
|
|
337
|
+
|
|
338
|
+
return TemplateResponse(request, 'bom/signup.html', locals())
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@login_required
|
|
342
|
+
def bom_signup(request):
|
|
343
|
+
user = request.user
|
|
344
|
+
organization = user.bom_profile().organization
|
|
345
|
+
title = 'Set Up Your BOM Organization'
|
|
346
|
+
|
|
347
|
+
if organization is not None:
|
|
348
|
+
return HttpResponseRedirect(reverse('bom:home'))
|
|
349
|
+
|
|
350
|
+
return TemplateResponse(request, 'bom/bom-signup.html', locals())
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@login_required
|
|
354
|
+
def bom_settings(request, tab_anchor=None):
|
|
355
|
+
user = request.user
|
|
356
|
+
profile = user.bom_profile()
|
|
357
|
+
organization = profile.organization
|
|
358
|
+
if organization is None:
|
|
359
|
+
return HttpResponseRedirect(reverse('bom:home'))
|
|
360
|
+
|
|
361
|
+
title = 'Settings'
|
|
362
|
+
owner = organization.owner
|
|
363
|
+
name = 'settings'
|
|
364
|
+
|
|
365
|
+
part_classes = PartClass.objects.all().filter(organization=organization)
|
|
366
|
+
property_definitions = PartRevisionPropertyDefinition.objects.available_to(organization=organization).order_by(
|
|
367
|
+
'name')
|
|
368
|
+
quantities_of_measure = QuantityOfMeasure.objects.available_to(organization=organization).order_by('name')
|
|
369
|
+
|
|
370
|
+
users_in_organization = User.objects.filter(
|
|
371
|
+
id__in=UserMeta.objects.filter(organization=organization).values_list('user', flat=True)).order_by(
|
|
372
|
+
'first_name', 'last_name', 'email')
|
|
373
|
+
users_in_organization_count = users_in_organization.count()
|
|
374
|
+
has_member_capacity = users_in_organization_count < organization.subscription_quantity
|
|
375
|
+
# Seats available for adding new members (never negative)
|
|
376
|
+
seats_available = max(organization.subscription_quantity - users_in_organization_count, 0)
|
|
377
|
+
is_pro = organization.subscription == constants.SUBSCRIPTION_TYPE_PRO
|
|
378
|
+
user_can_manage_members = request.user.has_perm('bom.manage_members', organization)
|
|
379
|
+
google_authentication = UserSocialAuth.objects.filter(user=user).first()
|
|
380
|
+
|
|
381
|
+
organization_parts_count = Part.objects.filter(organization=organization).count()
|
|
382
|
+
|
|
383
|
+
USER_TAB = 'user'
|
|
384
|
+
ORGANIZATION_TAB = 'organization'
|
|
385
|
+
INDABOM_TAB = 'indabom'
|
|
386
|
+
|
|
387
|
+
if request.method == 'POST':
|
|
388
|
+
part_class_action_ids = request.POST.getlist('actions')
|
|
389
|
+
part_class_action = request.POST.get('part-class-action')
|
|
390
|
+
if 'submit-edit-user' in request.POST:
|
|
391
|
+
tab_anchor = USER_TAB
|
|
392
|
+
user_form = UserForm(request.POST, instance=user)
|
|
393
|
+
if user_form.is_valid():
|
|
394
|
+
user = user_form.save()
|
|
395
|
+
else:
|
|
396
|
+
messages.error(request, user_form.errors)
|
|
397
|
+
|
|
398
|
+
elif 'refresh-edit-user' in request.POST:
|
|
399
|
+
tab_anchor = USER_TAB
|
|
400
|
+
user_form = UserForm(instance=user)
|
|
401
|
+
|
|
402
|
+
elif 'submit-add-user' in request.POST:
|
|
403
|
+
tab_anchor = ORGANIZATION_TAB
|
|
404
|
+
if not is_pro:
|
|
405
|
+
messages.error(request, "You need a Pro subscription to add users.")
|
|
406
|
+
elif not user_can_manage_members:
|
|
407
|
+
messages.error(request, "You are not allowed to manage users, contact your organization admin.")
|
|
408
|
+
elif not has_member_capacity:
|
|
409
|
+
messages.error(request,
|
|
410
|
+
"You have reached your organization's member capacity. Manage subscription to add more members.")
|
|
411
|
+
else:
|
|
412
|
+
user_add_form = UserAddForm(request.POST, organization=organization)
|
|
413
|
+
if user_add_form.is_valid():
|
|
414
|
+
added_user_profile = user_add_form.save()
|
|
415
|
+
messages.info(request, f"Added {added_user_profile.user.first_name} {added_user_profile.user.last_name} to your organization.")
|
|
416
|
+
else:
|
|
417
|
+
for field, errors in user_add_form.errors.items():
|
|
418
|
+
for error in errors:
|
|
419
|
+
messages.error(request, f"{field.capitalize()}: {error}")
|
|
420
|
+
users_in_organization.all()
|
|
421
|
+
users_in_organization_count = users_in_organization.count()
|
|
422
|
+
has_member_capacity = users_in_organization_count < organization.subscription_quantity
|
|
423
|
+
seats_available = max(organization.subscription_quantity - users_in_organization_count, 0)
|
|
424
|
+
elif 'clear-add-user' in request.POST:
|
|
425
|
+
tab_anchor = ORGANIZATION_TAB
|
|
426
|
+
user_add_form = UserAddForm()
|
|
427
|
+
|
|
428
|
+
elif 'submit-remove-user' in request.POST:
|
|
429
|
+
tab_anchor = ORGANIZATION_TAB
|
|
430
|
+
for item in request.POST:
|
|
431
|
+
if 'remove_user_meta_id_' in item:
|
|
432
|
+
user_meta_id = item.partition('remove_user_meta_id_')[2]
|
|
433
|
+
try:
|
|
434
|
+
user_meta = UserMeta.objects.get(id=user_meta_id, organization=organization)
|
|
435
|
+
if user_meta.user == organization.owner:
|
|
436
|
+
messages.error(request, "Can't remove organization owner.")
|
|
437
|
+
else:
|
|
438
|
+
user_meta.organization = None
|
|
439
|
+
user_meta.role = ''
|
|
440
|
+
user_meta.save()
|
|
441
|
+
except UserMeta.DoesNotExist:
|
|
442
|
+
messages.error(request, "No user found with given id {}.".format(user_meta_id))
|
|
443
|
+
users_in_organization.all()
|
|
444
|
+
users_in_organization_count = users_in_organization.count()
|
|
445
|
+
has_member_capacity = users_in_organization_count < organization.subscription_quantity
|
|
446
|
+
seats_available = max(organization.subscription_quantity - users_in_organization_count, 0)
|
|
447
|
+
elif 'submit-edit-organization' in request.POST:
|
|
448
|
+
tab_anchor = ORGANIZATION_TAB
|
|
449
|
+
organization_form = OrganizationFormEditSettings(request.POST, instance=organization, user=user)
|
|
450
|
+
if organization_form.is_valid():
|
|
451
|
+
organization_form.save()
|
|
452
|
+
else:
|
|
453
|
+
messages.error(request, organization_form.errors)
|
|
454
|
+
|
|
455
|
+
elif 'refresh-edit-organization' in request.POST:
|
|
456
|
+
tab_anchor = ORGANIZATION_TAB
|
|
457
|
+
organization_form = OrganizationFormEditSettings(instance=organization, user=user)
|
|
458
|
+
|
|
459
|
+
elif 'submit-number-item-len' in request.POST:
|
|
460
|
+
tab_anchor = INDABOM_TAB
|
|
461
|
+
organization_number_len_form = OrganizationNumberLenForm(request.POST, instance=organization)
|
|
462
|
+
if organization_number_len_form.is_valid():
|
|
463
|
+
organization_number_len_form.save()
|
|
464
|
+
else:
|
|
465
|
+
messages.error(request, organization_number_len_form.errors)
|
|
466
|
+
|
|
467
|
+
elif 'refresh-number-item-len' in request.POST:
|
|
468
|
+
tab_anchor = INDABOM_TAB
|
|
469
|
+
organization_number_len_form = OrganizationNumberLenForm(organization)
|
|
470
|
+
|
|
471
|
+
elif 'submit-part-class-create' in request.POST:
|
|
472
|
+
tab_anchor = INDABOM_TAB
|
|
473
|
+
part_class_form = PartClassForm(request.POST, request.FILES, organization=organization)
|
|
474
|
+
if part_class_form.is_valid():
|
|
475
|
+
part_class_form.save()
|
|
476
|
+
else:
|
|
477
|
+
messages.error(request, part_class_form.errors)
|
|
478
|
+
|
|
479
|
+
elif 'cancel-part-class-create' in request.POST:
|
|
480
|
+
tab_anchor = INDABOM_TAB
|
|
481
|
+
part_class_form = PartClassForm(organization=organization)
|
|
482
|
+
|
|
483
|
+
elif 'submit-part-class-upload' in request.POST and request.FILES.get('file') is not None:
|
|
484
|
+
tab_anchor = INDABOM_TAB
|
|
485
|
+
part_class_csv_form = PartClassCSVForm(request.POST, request.FILES, organization=organization)
|
|
486
|
+
if part_class_csv_form.is_valid():
|
|
487
|
+
messages.info(request, f'Successfully uploaded {len(part_class_csv_form.successes)} part classes.')
|
|
488
|
+
for warning in part_class_csv_form.warnings:
|
|
489
|
+
messages.warning(request, warning)
|
|
490
|
+
else:
|
|
491
|
+
messages.error(request, part_class_csv_form.errors)
|
|
492
|
+
|
|
493
|
+
elif 'submit-part-class-export' in request.POST:
|
|
494
|
+
response = HttpResponse(content_type='text/csv')
|
|
495
|
+
response['Content-Disposition'] = 'attachment; filename="indabom_parts_search.csv"'
|
|
496
|
+
csv_headers = PartClassesCSVHeaders()
|
|
497
|
+
writer = csv.DictWriter(response, fieldnames=csv_headers.get_default_all())
|
|
498
|
+
writer.writeheader()
|
|
499
|
+
part_classes = PartClass.objects.filter(organization=organization)
|
|
500
|
+
for part_class in part_classes:
|
|
501
|
+
row = {}
|
|
502
|
+
for field_name in csv_headers.get_default_all():
|
|
503
|
+
attr = getattr(part_class, field_name)
|
|
504
|
+
row.update({csv_headers.get_default(field_name): attr if attr is not None else ''})
|
|
505
|
+
writer.writerow({k: smart_str(v) for k, v in row.items()})
|
|
506
|
+
return response
|
|
507
|
+
|
|
508
|
+
elif 'part-class-action' in request.POST and part_class_action is not None:
|
|
509
|
+
if len(part_class_action_ids) <= 0:
|
|
510
|
+
messages.warning(request, "No action was taken because no part classes were selected. Select part classes by checking the checkboxes below.")
|
|
511
|
+
elif part_class_action == 'submit-part-class-enable-mouser':
|
|
512
|
+
tab_anchor = INDABOM_TAB
|
|
513
|
+
PartClass.objects.filter(id__in=part_class_action_ids).update(mouser_enabled=True)
|
|
514
|
+
elif part_class_action == 'submit-part-class-disable-mouser':
|
|
515
|
+
tab_anchor = INDABOM_TAB
|
|
516
|
+
PartClass.objects.filter(id__in=part_class_action_ids).update(mouser_enabled=False)
|
|
517
|
+
elif part_class_action == 'submit-part-class-delete':
|
|
518
|
+
tab_anchor = INDABOM_TAB
|
|
519
|
+
try:
|
|
520
|
+
PartClass.objects.filter(id__in=part_class_action_ids).delete()
|
|
521
|
+
except PartClass.DoesNotExist as err:
|
|
522
|
+
messages.error(request, f"No part class found: {err}")
|
|
523
|
+
except ProtectedError as err:
|
|
524
|
+
messages.error(request, f"Cannot delete a part class because it has parts. You must delete those parts first. {err}")
|
|
525
|
+
elif 'change-number-scheme' in request.POST:
|
|
526
|
+
tab_anchor = INDABOM_TAB
|
|
527
|
+
if organization_parts_count > 0:
|
|
528
|
+
messages.error(request, f"Please export, then delete all of your {organization_parts_count} parts before changing your organization's number scheme.")
|
|
529
|
+
else:
|
|
530
|
+
if organization.number_scheme == constants.NUMBER_SCHEME_SEMI_INTELLIGENT:
|
|
531
|
+
organization.number_scheme = constants.NUMBER_SCHEME_INTELLIGENT
|
|
532
|
+
organization.number_item_len = 128
|
|
533
|
+
else:
|
|
534
|
+
organization.number_scheme = constants.NUMBER_SCHEME_SEMI_INTELLIGENT
|
|
535
|
+
organization.number_item_len = 3
|
|
536
|
+
organization.save()
|
|
537
|
+
elif 'submit-leave-organization' in request.POST:
|
|
538
|
+
if organization.owner == user:
|
|
539
|
+
messages.error(request, "You are the owner of the organization. For now we're not letting owners leave their organization. This will change in the future. Contact info@indabom.com "
|
|
540
|
+
"if you want us to manually remove you from your organization.")
|
|
541
|
+
else:
|
|
542
|
+
profile.organization = None
|
|
543
|
+
profile.save()
|
|
544
|
+
if users_in_organization == 0:
|
|
545
|
+
organization.delete()
|
|
546
|
+
else:
|
|
547
|
+
messages.warning(request, "No action was taken because no form field was submitted.")
|
|
548
|
+
|
|
549
|
+
user_form = UserForm(instance=user)
|
|
550
|
+
user_add_form = UserAddForm()
|
|
551
|
+
user_add_form_action = reverse('bom:settings', kwargs={'tab_anchor': ORGANIZATION_TAB})
|
|
552
|
+
user_meta_form = UserMetaForm()
|
|
553
|
+
|
|
554
|
+
organization_form = OrganizationFormEditSettings(instance=organization, user=user)
|
|
555
|
+
organization_number_len_form = OrganizationNumberLenForm(instance=organization)
|
|
556
|
+
part_class_form = PartClassForm(organization=organization)
|
|
557
|
+
part_class_form_action = reverse('bom:settings', kwargs={'tab_anchor': INDABOM_TAB})
|
|
558
|
+
part_class_csv_form = PartClassCSVForm(organization=organization)
|
|
559
|
+
|
|
560
|
+
return TemplateResponse(request, 'bom/settings.html', locals())
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
564
|
+
def manufacturers(request):
|
|
565
|
+
profile = request.user.bom_profile()
|
|
566
|
+
organization = profile.organization
|
|
567
|
+
name = 'manufacturers'
|
|
568
|
+
if not organization:
|
|
569
|
+
return HttpResponseRedirect(reverse('bom:organization-create'))
|
|
570
|
+
|
|
571
|
+
query = request.GET.get('q', '')
|
|
572
|
+
title = f'{organization.name}\'s Manufacturers'
|
|
573
|
+
|
|
574
|
+
if query:
|
|
575
|
+
title += ' - Search Results'
|
|
576
|
+
|
|
577
|
+
manufacturers = Manufacturer.objects.filter(organization=organization, name__icontains=query).annotate(manufacturerpart_count=Count('manufacturerpart')).order_by('name')
|
|
578
|
+
|
|
579
|
+
autocomplete_dict = {}
|
|
580
|
+
for manufacturer in manufacturers:
|
|
581
|
+
autocomplete_dict.update({manufacturer.name: None})
|
|
582
|
+
autocomplete = dumps(autocomplete_dict)
|
|
583
|
+
|
|
584
|
+
paginator = Paginator(manufacturers, 50)
|
|
585
|
+
|
|
586
|
+
page = request.GET.get('page')
|
|
587
|
+
try:
|
|
588
|
+
manufacturers = paginator.page(page)
|
|
589
|
+
except PageNotAnInteger:
|
|
590
|
+
manufacturers = paginator.page(1)
|
|
591
|
+
except EmptyPage:
|
|
592
|
+
manufacturers = paginator.page(paginator.num_pages)
|
|
593
|
+
|
|
594
|
+
return TemplateResponse(request, 'bom/manufacturers.html', locals())
|
|
595
|
+
|
|
596
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
597
|
+
def manufacturer_info(request, manufacturer_id):
|
|
598
|
+
user = request.user
|
|
599
|
+
profile = user.bom_profile()
|
|
600
|
+
organization = profile.organization
|
|
601
|
+
|
|
602
|
+
manufacturer = get_object_or_404(Manufacturer, pk=manufacturer_id)
|
|
603
|
+
|
|
604
|
+
if manufacturer.organization != organization:
|
|
605
|
+
messages.error(request, "Can't access a manufacturer that is not yours!")
|
|
606
|
+
return HttpResponseRedirect(reverse('bom:home'))
|
|
607
|
+
|
|
608
|
+
manufacturer_parts = ManufacturerPart.objects.filter(manufacturer=manufacturer).order_by('manufacturer_part_number')
|
|
609
|
+
|
|
610
|
+
return TemplateResponse(request, 'bom/manufacturer-info.html', locals())
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
614
|
+
def manufacturer_edit(request, manufacturer_id):
|
|
615
|
+
user = request.user
|
|
616
|
+
profile = user.bom_profile()
|
|
617
|
+
organization = profile.organization
|
|
618
|
+
|
|
619
|
+
manufacturer = get_object_or_404(Manufacturer, pk=manufacturer_id)
|
|
620
|
+
title = 'Edit Manufacturer'
|
|
621
|
+
action = reverse('bom:manufacturer-edit', kwargs={'manufacturer_id': manufacturer_id})
|
|
622
|
+
|
|
623
|
+
if request.method == 'POST':
|
|
624
|
+
form = ManufacturerForm(request.POST, instance=manufacturer)
|
|
625
|
+
if form.is_valid():
|
|
626
|
+
form.save()
|
|
627
|
+
return HttpResponseRedirect(reverse('bom:manufacturer-info', kwargs={'manufacturer_id': manufacturer_id}))
|
|
628
|
+
else:
|
|
629
|
+
form = ManufacturerForm(instance=manufacturer)
|
|
630
|
+
|
|
631
|
+
return TemplateResponse(request, 'bom/bom-form.html', locals())
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
635
|
+
@organization_admin
|
|
636
|
+
def manufacturer_delete(request, manufacturer_id):
|
|
637
|
+
manufacturer = get_object_or_404(Manufacturer, pk=manufacturer_id)
|
|
638
|
+
manufacturer.delete()
|
|
639
|
+
return HttpResponseRedirect(reverse('bom:manufacturers'))
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
643
|
+
def sellers(request):
|
|
644
|
+
profile = request.user.bom_profile()
|
|
645
|
+
organization = profile.organization
|
|
646
|
+
name = 'sellers'
|
|
647
|
+
query = request.GET.get('q', '')
|
|
648
|
+
title = f'{organization.name}\'s Sellers'
|
|
649
|
+
|
|
650
|
+
if query:
|
|
651
|
+
title += ' - Search Results'
|
|
652
|
+
|
|
653
|
+
sellers = Seller.objects.filter(organization=organization, name__icontains=query).annotate(sellerpart_count=Count('sellerpart')).order_by('name')
|
|
654
|
+
|
|
655
|
+
autocomplete_dict = {}
|
|
656
|
+
for seller in sellers:
|
|
657
|
+
autocomplete_dict.update({seller.name: None})
|
|
658
|
+
|
|
659
|
+
autocomplete = dumps(autocomplete_dict)
|
|
660
|
+
|
|
661
|
+
paginator = Paginator(sellers, 50)
|
|
662
|
+
page = request.GET.get('page')
|
|
663
|
+
try:
|
|
664
|
+
sellers = paginator.page(page)
|
|
665
|
+
except PageNotAnInteger:
|
|
666
|
+
sellers = paginator.page(1)
|
|
667
|
+
except EmptyPage:
|
|
668
|
+
sellers = paginator.page(paginator.num_pages)
|
|
669
|
+
|
|
670
|
+
return TemplateResponse(request, 'bom/sellers.html', locals())
|
|
671
|
+
|
|
672
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
673
|
+
def seller_info(request, seller_id):
|
|
674
|
+
user = request.user
|
|
675
|
+
profile = user.bom_profile()
|
|
676
|
+
organization = profile.organization
|
|
677
|
+
|
|
678
|
+
seller = get_object_or_404(Seller, pk=seller_id)
|
|
679
|
+
|
|
680
|
+
if seller.organization != organization:
|
|
681
|
+
messages.error(request, "Can't access a seller that is not yours!")
|
|
682
|
+
return HttpResponseRedirect(reverse('bom:home'))
|
|
683
|
+
|
|
684
|
+
seller_parts = SellerPart.objects.filter(seller=seller).order_by('manufacturer_part__part__number_class',
|
|
685
|
+
'manufacturer_part__part__number_item',
|
|
686
|
+
'manufacturer_part__part__number_variation',
|
|
687
|
+
'manufacturer_part__manufacturer__name',
|
|
688
|
+
'manufacturer_part__manufacturer_part_number',
|
|
689
|
+
'seller__name',
|
|
690
|
+
'minimum_order_quantity')
|
|
691
|
+
return TemplateResponse(request, 'bom/seller-info.html', locals())
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
695
|
+
def seller_edit(request, seller_id):
|
|
696
|
+
user = request.user
|
|
697
|
+
profile = user.bom_profile()
|
|
698
|
+
organization = profile.organization
|
|
699
|
+
|
|
700
|
+
seller = get_object_or_404(Seller, pk=seller_id)
|
|
701
|
+
title = 'Edit Seller'
|
|
702
|
+
action = reverse('bom:seller-edit', kwargs={'seller_id': seller_id})
|
|
703
|
+
|
|
704
|
+
if request.method == 'POST':
|
|
705
|
+
form = SellerForm(request.POST, instance=seller)
|
|
706
|
+
if form.is_valid():
|
|
707
|
+
form.save()
|
|
708
|
+
return HttpResponseRedirect(reverse('bom:seller-info', kwargs={'seller_id': seller_id}))
|
|
709
|
+
else:
|
|
710
|
+
form = SellerForm(instance=seller)
|
|
711
|
+
|
|
712
|
+
return TemplateResponse(request, 'bom/bom-form.html', locals())
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
716
|
+
@organization_admin
|
|
717
|
+
def seller_delete(request, seller_id):
|
|
718
|
+
seller = get_object_or_404(Seller, pk=seller_id)
|
|
719
|
+
seller.delete()
|
|
720
|
+
return HttpResponseRedirect(reverse('bom:sellers'))
|
|
721
|
+
|
|
722
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
723
|
+
def user_meta_edit(request, user_meta_id):
|
|
724
|
+
user_meta = get_object_or_404(UserMeta, pk=user_meta_id)
|
|
725
|
+
user = user_meta.user
|
|
726
|
+
organization = user_meta.organization
|
|
727
|
+
title = f'Manage Member'
|
|
728
|
+
|
|
729
|
+
if request.method == 'POST':
|
|
730
|
+
form = UserAddForm(request.POST, instance=user_meta, organization=organization, exclude_username=True)
|
|
731
|
+
if form.is_valid():
|
|
732
|
+
form.save()
|
|
733
|
+
return HttpResponseRedirect(reverse('bom:settings', kwargs={'tab_anchor': 'organization'}))
|
|
734
|
+
return TemplateResponse(request, 'bom/edit-user-meta.html', locals())
|
|
735
|
+
else:
|
|
736
|
+
form = UserAddForm(instance=user_meta, organization=organization, exclude_username=True)
|
|
737
|
+
return TemplateResponse(request, 'bom/edit-user-meta.html', locals())
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
741
|
+
def part_info(request, part_id, part_revision_id=None):
|
|
742
|
+
tab_anchor = request.GET.get('tab_anchor', None)
|
|
743
|
+
|
|
744
|
+
user = request.user
|
|
745
|
+
profile = user.bom_profile()
|
|
746
|
+
organization = profile.organization
|
|
747
|
+
|
|
748
|
+
part = get_object_or_404(Part, pk=part_id)
|
|
749
|
+
|
|
750
|
+
part_revision = None
|
|
751
|
+
if part_revision_id is None:
|
|
752
|
+
part_revision = part.latest()
|
|
753
|
+
else:
|
|
754
|
+
part_revision = get_object_or_404(PartRevision, pk=part_revision_id)
|
|
755
|
+
|
|
756
|
+
try:
|
|
757
|
+
selected_rev_is_latest = part_revision.revision == part.latest().revision
|
|
758
|
+
except AttributeError:
|
|
759
|
+
selected_rev_is_latest = False
|
|
760
|
+
|
|
761
|
+
revisions = PartRevision.objects.filter(part=part_id).order_by('-id')
|
|
762
|
+
|
|
763
|
+
if part.organization != organization:
|
|
764
|
+
messages.error(request, "Can't access a part that is not yours!")
|
|
765
|
+
return HttpResponseRedirect(reverse('bom:home'))
|
|
766
|
+
|
|
767
|
+
qty_cache_key = str(part_id) + '_qty'
|
|
768
|
+
qty = cache.get(qty_cache_key, 100)
|
|
769
|
+
part_info_form = PartInfoForm(initial={'quantity': qty})
|
|
770
|
+
upload_file_to_part_form = FileForm()
|
|
771
|
+
|
|
772
|
+
if request.method == 'POST':
|
|
773
|
+
part_info_form = PartInfoForm(request.POST)
|
|
774
|
+
if part_info_form.is_valid():
|
|
775
|
+
qty = request.POST.get('quantity', 100)
|
|
776
|
+
|
|
777
|
+
try:
|
|
778
|
+
qty = int(qty)
|
|
779
|
+
except ValueError:
|
|
780
|
+
qty = 100
|
|
781
|
+
|
|
782
|
+
cache.set(qty_cache_key, qty, timeout=None)
|
|
783
|
+
|
|
784
|
+
try:
|
|
785
|
+
indented_bom = part_revision.indented(top_level_quantity=qty)
|
|
786
|
+
except (RuntimeError, RecursionError):
|
|
787
|
+
messages.error(request, "Error: infinite recursion in part relationship. Contact info@indabom.com to resolve.")
|
|
788
|
+
indented_bom = []
|
|
789
|
+
except AttributeError as err:
|
|
790
|
+
# No part revision found, that's OK
|
|
791
|
+
indented_bom = []
|
|
792
|
+
|
|
793
|
+
try:
|
|
794
|
+
flat_bom = part_revision.flat(top_level_quantity=qty)
|
|
795
|
+
except (RuntimeError, RecursionError):
|
|
796
|
+
messages.error(request, "Error: infinite recursion in part relationship. Contact info@indabom.com to resolve.")
|
|
797
|
+
flat_bom = []
|
|
798
|
+
except AttributeError as err:
|
|
799
|
+
# No part revision found, that's OK
|
|
800
|
+
flat_bom = []
|
|
801
|
+
|
|
802
|
+
try:
|
|
803
|
+
where_used = part_revision.where_used()
|
|
804
|
+
except AttributeError:
|
|
805
|
+
where_used = []
|
|
806
|
+
|
|
807
|
+
try:
|
|
808
|
+
mouser_parts = len(flat_bom.mouser_parts().keys()) > 0
|
|
809
|
+
except AttributeError:
|
|
810
|
+
mouser_parts = False
|
|
811
|
+
|
|
812
|
+
where_used_part = part.where_used()
|
|
813
|
+
seller_parts = part.seller_parts()
|
|
814
|
+
return TemplateResponse(request, 'bom/part-info.html', locals())
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
818
|
+
def part_export_bom(request, part_id=None, part_revision_id=None, flat=False, sourcing=False, sourcing_detailed=False):
|
|
819
|
+
user = request.user
|
|
820
|
+
profile = user.bom_profile()
|
|
821
|
+
organization = profile.organization
|
|
822
|
+
|
|
823
|
+
if part_id is not None:
|
|
824
|
+
part = get_object_or_404(Part, pk=part_id)
|
|
825
|
+
part_revision = part.latest()
|
|
826
|
+
elif part_revision_id is not None:
|
|
827
|
+
part_revision = get_object_or_404(PartRevision, pk=part_revision_id)
|
|
828
|
+
part = part_revision.part
|
|
829
|
+
else:
|
|
830
|
+
messages.error(request, "View requires part or part revision.")
|
|
831
|
+
return HttpResponseRedirect(request.META.get('HTTP_REFERER'), '/')
|
|
832
|
+
|
|
833
|
+
if part.organization != organization:
|
|
834
|
+
messages.error(request, "Cant export a part that is not yours!")
|
|
835
|
+
return HttpResponseRedirect(request.META.get('HTTP_REFERER'), '/')
|
|
836
|
+
|
|
837
|
+
response = HttpResponse(content_type='text/csv')
|
|
838
|
+
filename = f'indabom_export_{part.full_part_number()}_{"flat" if flat else "indented"}'
|
|
839
|
+
response['Content-Disposition'] = f'attachment; filename="{filename}.csv'
|
|
840
|
+
|
|
841
|
+
qty_cache_key = str(part_id) + '_qty'
|
|
842
|
+
qty = cache.get(qty_cache_key, 1000)
|
|
843
|
+
|
|
844
|
+
try:
|
|
845
|
+
if flat:
|
|
846
|
+
bom = part_revision.flat(top_level_quantity=qty)
|
|
847
|
+
else:
|
|
848
|
+
bom = part_revision.indented(top_level_quantity=qty)
|
|
849
|
+
except (RuntimeError, RecursionError):
|
|
850
|
+
messages.error(request, "Error: infinite recursion in part relationship. Contact info@indabom.com to resolve.")
|
|
851
|
+
bom = []
|
|
852
|
+
except AttributeError as err:
|
|
853
|
+
messages.error(request, err)
|
|
854
|
+
bom = []
|
|
855
|
+
|
|
856
|
+
if flat:
|
|
857
|
+
csv_headers = BOMFlatCSVHeaders()
|
|
858
|
+
else:
|
|
859
|
+
csv_headers = BOMIndentedCSVHeaders()
|
|
860
|
+
|
|
861
|
+
csv_headers_raw = csv_headers.get_default_all()
|
|
862
|
+
csv_rows = []
|
|
863
|
+
for _, item in bom.parts.items():
|
|
864
|
+
mapped_row = {}
|
|
865
|
+
raw_row = {k: smart_str(v) for k, v in item.as_dict_for_export().items()}
|
|
866
|
+
for kx, vx in raw_row.items():
|
|
867
|
+
if csv_headers.get_default(kx) is None: print ("NONE", kx)
|
|
868
|
+
mapped_row.update({csv_headers.get_default(kx): vx})
|
|
869
|
+
|
|
870
|
+
if sourcing_detailed:
|
|
871
|
+
for idx, sp in enumerate(item.seller_parts_for_export()):
|
|
872
|
+
if f'{ManufacturerPartCSVHeaders.all_headers_defns[0]}_{idx + 1}' not in csv_headers_raw:
|
|
873
|
+
csv_headers_raw.extend([f'{h}_{idx + 1}' for h in ManufacturerPartCSVHeaders.all_headers_defns])
|
|
874
|
+
csv_headers_raw.extend([f'{h}_{idx + 1}' for h in SellerPartCSVHeaders.all_headers_defns])
|
|
875
|
+
mapped_row.update({f'{k}_{idx + 1}': smart_str(v) for k, v in sp.items()})
|
|
876
|
+
elif sourcing:
|
|
877
|
+
for idx, mp in enumerate(item.manufacturer_parts_for_export()):
|
|
878
|
+
if f'{ManufacturerPartCSVHeaders.all_headers_defns[0]}_{idx + 1}' not in csv_headers_raw:
|
|
879
|
+
csv_headers_raw.extend([f'{h}_{idx + 1}' for h in ManufacturerPartCSVHeaders.all_headers_defns])
|
|
880
|
+
mapped_row.update({f'{k}_{idx + 1}': smart_str(v) for k, v in mp.items()})
|
|
881
|
+
|
|
882
|
+
csv_rows.append(mapped_row)
|
|
883
|
+
|
|
884
|
+
writer = csv.DictWriter(response, fieldnames=csv_headers_raw)
|
|
885
|
+
writer.writeheader()
|
|
886
|
+
writer.writerows(csv_rows)
|
|
887
|
+
|
|
888
|
+
return response
|
|
889
|
+
|
|
890
|
+
# @login_required
|
|
891
|
+
# def part_export_bom_flat(request, part_revision_id):
|
|
892
|
+
# user = request.user
|
|
893
|
+
# profile = user.bom_profile()
|
|
894
|
+
# organization = profile.organization
|
|
895
|
+
#
|
|
896
|
+
# part_revision = get_object_or_404(PartRevision, pk=part_revision_id)
|
|
897
|
+
#
|
|
898
|
+
# if part_revision.part.organization != organization:
|
|
899
|
+
# messages.error(request, "Cant export a part that is not yours!")
|
|
900
|
+
# return HttpResponseRedirect(request.META.get('HTTP_REFERER'), '/')
|
|
901
|
+
#
|
|
902
|
+
# response = HttpResponse(content_type='text/csv')
|
|
903
|
+
# response['Content-Disposition'] = 'attachment; filename="{}_indabom_parts_flat.csv"'.format(
|
|
904
|
+
# part_revision.part.full_part_number())
|
|
905
|
+
#
|
|
906
|
+
# # As compared to indented bom, show all references for a subpart as a single item and
|
|
907
|
+
# # don't show do_not_load status at all because it won't be clear as to which subpart reference
|
|
908
|
+
# # the do_not_load refers to.
|
|
909
|
+
# qty_cache_key = str(part_revision.part.id) + '_qty'
|
|
910
|
+
# qty = cache.get(qty_cache_key, 1000)
|
|
911
|
+
#
|
|
912
|
+
# try:
|
|
913
|
+
# bom = part_revision.flat(top_level_quantity=qty)
|
|
914
|
+
# except (RuntimeError, RecursionError):
|
|
915
|
+
# messages.error(request, "Error: infinite recursion in part relationship. Contact info@indabom.com to resolve.")
|
|
916
|
+
# bom = []
|
|
917
|
+
# except AttributeError as err:
|
|
918
|
+
# messages.error(request, err)
|
|
919
|
+
# bom = []
|
|
920
|
+
#
|
|
921
|
+
# csv_headers = BOMFlatCSVHeaders()
|
|
922
|
+
# writer = csv.DictWriter(response, fieldnames=csv_headers.get_default_all())
|
|
923
|
+
# writer.writeheader()
|
|
924
|
+
#
|
|
925
|
+
# for _, item in bom.parts.items():
|
|
926
|
+
# mapped_row = {}
|
|
927
|
+
# raw_row = {k: smart_str(v) for k, v in item.as_dict_for_export().items()}
|
|
928
|
+
# for kx, vx in raw_row.items():
|
|
929
|
+
# if csv_headers.get_default(kx) is None: print ("NONE", kx)
|
|
930
|
+
# mapped_row.update({csv_headers.get_default(kx): vx})
|
|
931
|
+
# writer.writerow({k: smart_str(v) for k, v in mapped_row.items()})
|
|
932
|
+
#
|
|
933
|
+
# return response
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
937
|
+
def upload_bom(request):
|
|
938
|
+
user = request.user
|
|
939
|
+
profile = user.bom_profile()
|
|
940
|
+
organization = profile.organization
|
|
941
|
+
title = 'Upload Bill of Materials'
|
|
942
|
+
|
|
943
|
+
if request.method == 'POST' and 'file' in request.FILES and request.FILES['file'] is not None:
|
|
944
|
+
upload_bom_form = UploadBOMForm(request.POST, organization=organization)
|
|
945
|
+
if upload_bom_form.is_valid():
|
|
946
|
+
bom_csv_form = BOMCSVForm(request.POST, request.FILES, parent_part=upload_bom_form.parent_part, organization=organization)
|
|
947
|
+
if bom_csv_form.is_valid():
|
|
948
|
+
for success in bom_csv_form.successes:
|
|
949
|
+
messages.info(request, success)
|
|
950
|
+
for warning in bom_csv_form.warnings:
|
|
951
|
+
messages.info(request, warning)
|
|
952
|
+
else:
|
|
953
|
+
messages.error(request, bom_csv_form.errors)
|
|
954
|
+
else:
|
|
955
|
+
messages.error(request, upload_bom_form.errors)
|
|
956
|
+
else:
|
|
957
|
+
upload_bom_form = UploadBOMForm(organization=organization, initial={'organization': organization})
|
|
958
|
+
bom_csv_form = BOMCSVForm()
|
|
959
|
+
|
|
960
|
+
return TemplateResponse(request, 'bom/upload-bom.html', locals())
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
964
|
+
def part_upload_bom(request, part_id):
|
|
965
|
+
user = request.user
|
|
966
|
+
profile = user.bom_profile()
|
|
967
|
+
organization = profile.organization
|
|
968
|
+
|
|
969
|
+
try:
|
|
970
|
+
parent_part = Part.objects.get(id=part_id)
|
|
971
|
+
except Part.DoesNotExist:
|
|
972
|
+
messages.error(request, "No part found with given part_id {}.".format(part_id))
|
|
973
|
+
return HttpResponseRedirect(request.META.get('HTTP_REFERER'), '/')
|
|
974
|
+
|
|
975
|
+
if request.method == 'POST' and request.FILES['file'] is not None:
|
|
976
|
+
bom_csv_form = BOMCSVForm(request.POST, request.FILES, parent_part=parent_part, organization=organization)
|
|
977
|
+
if bom_csv_form.is_valid():
|
|
978
|
+
for success in bom_csv_form.successes:
|
|
979
|
+
messages.info(request, success)
|
|
980
|
+
for warning in bom_csv_form.warnings:
|
|
981
|
+
messages.info(request, warning)
|
|
982
|
+
else:
|
|
983
|
+
messages.error(request, bom_csv_form.errors)
|
|
984
|
+
else:
|
|
985
|
+
upload_bom_form = UploadBOMForm(initial={'organization': organization})
|
|
986
|
+
bom_csv_form = BOMCSVForm()
|
|
987
|
+
|
|
988
|
+
return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse('bom:home')), locals())
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
992
|
+
def upload_parts_help(request):
|
|
993
|
+
return TemplateResponse(request, 'bom/upload-parts-help.html', locals())
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
997
|
+
def upload_parts(request):
|
|
998
|
+
user = request.user
|
|
999
|
+
profile = user.bom_profile()
|
|
1000
|
+
organization = profile.organization
|
|
1001
|
+
title = 'Upload Parts'
|
|
1002
|
+
|
|
1003
|
+
if request.method == 'POST' and request.FILES['file'] is not None:
|
|
1004
|
+
form = PartCSVForm(request.POST, request.FILES, organization=organization)
|
|
1005
|
+
if form.is_valid():
|
|
1006
|
+
for success in form.successes:
|
|
1007
|
+
messages.info(request, success)
|
|
1008
|
+
for warning in form.warnings:
|
|
1009
|
+
messages.warning(request, warning)
|
|
1010
|
+
else:
|
|
1011
|
+
messages.error(request, form.errors)
|
|
1012
|
+
else:
|
|
1013
|
+
form = FileForm()
|
|
1014
|
+
if organization.number_scheme == constants.NUMBER_SCHEME_SEMI_INTELLIGENT and organization.partclass_set.count() <= 0:
|
|
1015
|
+
messages.warning(request, f'!! Warning !! Before you upload parts, you must create any part classes. You can do this in Settings > Indabom.')
|
|
1016
|
+
return TemplateResponse(request, 'bom/upload-parts.html', locals())
|
|
1017
|
+
|
|
1018
|
+
return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse('bom:home')))
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1022
|
+
def export_part_list(request):
|
|
1023
|
+
user = request.user
|
|
1024
|
+
profile = user.bom_profile()
|
|
1025
|
+
organization = profile.organization
|
|
1026
|
+
|
|
1027
|
+
response = HttpResponse(content_type='text/csv')
|
|
1028
|
+
response['Content-Disposition'] = 'attachment; filename="indabom_parts.csv"'
|
|
1029
|
+
|
|
1030
|
+
parts = Part.objects.filter(
|
|
1031
|
+
organization=organization).order_by(
|
|
1032
|
+
'number_class__code',
|
|
1033
|
+
'number_item',
|
|
1034
|
+
'number_variation')
|
|
1035
|
+
|
|
1036
|
+
csv_headers = organization.part_list_csv_headers()
|
|
1037
|
+
writer = csv.DictWriter(response, fieldnames=csv_headers.get_default_all())
|
|
1038
|
+
writer.writeheader()
|
|
1039
|
+
for item in parts:
|
|
1040
|
+
try:
|
|
1041
|
+
latest_rev = item.latest()
|
|
1042
|
+
row = {
|
|
1043
|
+
csv_headers.get_default('part_number'): item.full_part_number(),
|
|
1044
|
+
csv_headers.get_default('part_revision'): latest_rev.revision if latest_rev else '',
|
|
1045
|
+
csv_headers.get_default('part_manufacturer'): item.primary_manufacturer_part.manufacturer.name if item.primary_manufacturer_part is not None and item.primary_manufacturer_part.manufacturer is not None else '',
|
|
1046
|
+
csv_headers.get_default('part_manufacturer_part_number'): item.primary_manufacturer_part.manufacturer_part_number if item.primary_manufacturer_part is not None and item.primary_manufacturer_part.manufacturer is not None else '',
|
|
1047
|
+
}
|
|
1048
|
+
for field_name in csv_headers.get_default_all():
|
|
1049
|
+
if field_name not in csv_headers.get_defaults_list(
|
|
1050
|
+
['part_number', 'part_category', 'part_synopsis', 'part_revision', 'part_manufacturer',
|
|
1051
|
+
'part_manufacturer_part_number', ]):
|
|
1052
|
+
attr = latest_rev.get_field_value(field_name) if latest_rev else None
|
|
1053
|
+
row.update({csv_headers.get_default(field_name): attr if attr is not None else ''})
|
|
1054
|
+
writer.writerow({k: smart_str(v) for k, v in row.items()})
|
|
1055
|
+
|
|
1056
|
+
except AttributeError as e:
|
|
1057
|
+
messages.warning(request, "No change history for part: {}. Can't export.".format(item.full_part_number()))
|
|
1058
|
+
|
|
1059
|
+
return response
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1063
|
+
def create_part(request):
|
|
1064
|
+
user = request.user
|
|
1065
|
+
profile = user.bom_profile()
|
|
1066
|
+
organization = profile.organization
|
|
1067
|
+
|
|
1068
|
+
title = 'Create New Part'
|
|
1069
|
+
|
|
1070
|
+
PartForm = part_form_from_organization(organization)
|
|
1071
|
+
|
|
1072
|
+
if organization.number_scheme == constants.NUMBER_SCHEME_SEMI_INTELLIGENT and PartClass.objects.count() == 0:
|
|
1073
|
+
messages.info(request, f'Welcome to IndaBOM! Before you create your first part, you must create your first part class. '
|
|
1074
|
+
f'<a href="{reverse("bom:help")}#part-numbering" target="_blank">What is a part class?</a>')
|
|
1075
|
+
return HttpResponseRedirect(reverse('bom:settings', kwargs={'tab_anchor': 'indabom'}))
|
|
1076
|
+
|
|
1077
|
+
if request.method == 'POST':
|
|
1078
|
+
part_form = PartForm(request.POST, organization=organization)
|
|
1079
|
+
manufacturer_form = ManufacturerForm(request.POST)
|
|
1080
|
+
manufacturer_part_form = ManufacturerPartForm(request.POST, organization=organization)
|
|
1081
|
+
part_revision_form = PartRevisionForm(request.POST, organization=organization)
|
|
1082
|
+
# Checking if part form is valid checks for number uniqueness
|
|
1083
|
+
if part_form.is_valid() and manufacturer_form.is_valid() and manufacturer_part_form.is_valid():
|
|
1084
|
+
mpn = manufacturer_part_form.cleaned_data['manufacturer_part_number']
|
|
1085
|
+
old_manufacturer = manufacturer_part_form.cleaned_data['manufacturer']
|
|
1086
|
+
new_manufacturer_name = manufacturer_form.cleaned_data['name']
|
|
1087
|
+
|
|
1088
|
+
manufacturer = None
|
|
1089
|
+
if mpn:
|
|
1090
|
+
if old_manufacturer and new_manufacturer_name == '':
|
|
1091
|
+
manufacturer = old_manufacturer
|
|
1092
|
+
elif new_manufacturer_name and new_manufacturer_name != '' and not old_manufacturer:
|
|
1093
|
+
manufacturer, created = Manufacturer.objects.get_or_create(name__iexact=new_manufacturer_name, organization=organization, defaults={'name': new_manufacturer_name})
|
|
1094
|
+
else:
|
|
1095
|
+
messages.error(request, "Either create a new manufacturer, or select an existing manufacturer.")
|
|
1096
|
+
return TemplateResponse(request, 'bom/create-part.html', locals())
|
|
1097
|
+
elif old_manufacturer or new_manufacturer_name != '':
|
|
1098
|
+
messages.warning(request, "No manufacturer was selected or created, no manufacturer part number was assigned.")
|
|
1099
|
+
new_part = part_form.save(commit=False)
|
|
1100
|
+
new_part.organization = organization
|
|
1101
|
+
|
|
1102
|
+
if organization.number_scheme == constants.NUMBER_SCHEME_INTELLIGENT:
|
|
1103
|
+
new_part.number_class = None
|
|
1104
|
+
new_part.number_variation = None
|
|
1105
|
+
|
|
1106
|
+
part_revision_form = PartRevisionForm(request.POST, part_class=new_part.number_class,
|
|
1107
|
+
organization=organization)
|
|
1108
|
+
if part_revision_form.is_valid():
|
|
1109
|
+
# Save the Part before the PartRevision, as this will again check for part
|
|
1110
|
+
# number uniqueness. This way if someone else(s) working concurrently is also
|
|
1111
|
+
# using the same part number, then only one person will succeed.
|
|
1112
|
+
try:
|
|
1113
|
+
new_part.save() # Database checks that the part number is still unique
|
|
1114
|
+
pr = part_revision_form.save(commit=False)
|
|
1115
|
+
pr.part = new_part # Associate PartRevision with Part
|
|
1116
|
+
pr.save()
|
|
1117
|
+
except IntegrityError as err:
|
|
1118
|
+
messages.error(request, "Error! Already created a part with part number {0}-{1}-{2}}".format(
|
|
1119
|
+
new_part.number_class.code, new_part.number_item, new_part.number_variation))
|
|
1120
|
+
return TemplateResponse(request, 'bom/create-part.html', locals())
|
|
1121
|
+
else:
|
|
1122
|
+
messages.error(request, part_revision_form.errors)
|
|
1123
|
+
return TemplateResponse(request, 'bom/create-part.html', locals())
|
|
1124
|
+
|
|
1125
|
+
manufacturer_part = None
|
|
1126
|
+
if manufacturer is not None:
|
|
1127
|
+
manufacturer_part, created = ManufacturerPart.objects.get_or_create(
|
|
1128
|
+
part=new_part,
|
|
1129
|
+
manufacturer_part_number='' if mpn == '' else mpn,
|
|
1130
|
+
manufacturer=manufacturer)
|
|
1131
|
+
|
|
1132
|
+
new_part.primary_manufacturer_part = manufacturer_part
|
|
1133
|
+
new_part.save()
|
|
1134
|
+
|
|
1135
|
+
return HttpResponseRedirect(reverse('bom:part-info', kwargs={'part_id': str(new_part.id)}))
|
|
1136
|
+
else:
|
|
1137
|
+
# Initialize organization in the form's model and in the form itself:
|
|
1138
|
+
part_form = PartForm(initial={'organization': organization}, organization=organization)
|
|
1139
|
+
part_revision_form = PartRevisionForm(initial={'revision': 1, 'organization': organization},
|
|
1140
|
+
organization=organization)
|
|
1141
|
+
manufacturer_form = ManufacturerForm(initial={'organization': organization})
|
|
1142
|
+
manufacturer_part_form = ManufacturerPartForm(organization=organization)
|
|
1143
|
+
|
|
1144
|
+
return TemplateResponse(request, 'bom/create-part.html', locals())
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1148
|
+
def part_edit(request, part_id):
|
|
1149
|
+
user = request.user
|
|
1150
|
+
profile = user.bom_profile()
|
|
1151
|
+
organization = profile.organization
|
|
1152
|
+
|
|
1153
|
+
part = get_object_or_404(Part, pk=part_id)
|
|
1154
|
+
title = 'Edit Part {}'.format(part.full_part_number())
|
|
1155
|
+
|
|
1156
|
+
action = reverse('bom:part-edit', kwargs={'part_id': part_id})
|
|
1157
|
+
|
|
1158
|
+
PartForm = part_form_from_organization(organization)
|
|
1159
|
+
|
|
1160
|
+
if request.method == 'POST':
|
|
1161
|
+
form = PartForm(request.POST, instance=part, organization=organization)
|
|
1162
|
+
if form.is_valid():
|
|
1163
|
+
form.save()
|
|
1164
|
+
return HttpResponseRedirect(reverse('bom:part-info', kwargs={'part_id': part_id}))
|
|
1165
|
+
else:
|
|
1166
|
+
form = PartForm(instance=part, organization=organization)
|
|
1167
|
+
|
|
1168
|
+
return TemplateResponse(request, 'bom/bom-form.html', locals())
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1172
|
+
def manage_bom(request, part_id, part_revision_id):
|
|
1173
|
+
user = request.user
|
|
1174
|
+
profile = user.bom_profile()
|
|
1175
|
+
organization = profile.organization
|
|
1176
|
+
|
|
1177
|
+
part = get_object_or_404(Part, pk=part_id)
|
|
1178
|
+
|
|
1179
|
+
part_revision = get_object_or_404(PartRevision, pk=part_revision_id)
|
|
1180
|
+
|
|
1181
|
+
title = 'Manage BOM for ' + part.full_part_number()
|
|
1182
|
+
|
|
1183
|
+
if part.organization != organization:
|
|
1184
|
+
messages.error(request, "Cant access a part that is not yours!")
|
|
1185
|
+
return HttpResponseRedirect(request.META.get('HTTP_REFERER'), '/')
|
|
1186
|
+
|
|
1187
|
+
add_subpart_form = AddSubpartForm(initial={'count': 1, }, organization=organization, part_id=part_id)
|
|
1188
|
+
upload_subparts_csv_form = FileForm()
|
|
1189
|
+
|
|
1190
|
+
qty_cache_key = str(part_id) + '_qty'
|
|
1191
|
+
qty = cache.get(qty_cache_key, 100)
|
|
1192
|
+
|
|
1193
|
+
try:
|
|
1194
|
+
indented_bom = part_revision.indented(top_level_quantity=qty)
|
|
1195
|
+
except (RuntimeError, RecursionError):
|
|
1196
|
+
messages.error(request, "Error: infinite recursion in part relationship. Contact info@indabom.com to resolve.")
|
|
1197
|
+
indented_bom = []
|
|
1198
|
+
except AttributeError as err:
|
|
1199
|
+
messages.error(request, err)
|
|
1200
|
+
indented_bom = []
|
|
1201
|
+
|
|
1202
|
+
references_seen = set()
|
|
1203
|
+
duplicate_references = set()
|
|
1204
|
+
for sp in part_revision.assembly.subparts.all():
|
|
1205
|
+
check_references_for_duplicates(sp.reference, references_seen, duplicate_references)
|
|
1206
|
+
|
|
1207
|
+
if len(duplicate_references) > 0:
|
|
1208
|
+
sorted_duplicate_references = sorted(duplicate_references, key=prep_for_sorting_nicely)
|
|
1209
|
+
messages.warning(request, "Warning: The following BOM references are associated with multiple parts: " + str(sorted_duplicate_references))
|
|
1210
|
+
|
|
1211
|
+
return TemplateResponse(request, 'bom/part-revision-manage-bom.html', locals())
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1215
|
+
@organization_admin
|
|
1216
|
+
def part_delete(request, part_id):
|
|
1217
|
+
part = get_object_or_404(Part, pk=part_id)
|
|
1218
|
+
part.delete()
|
|
1219
|
+
return HttpResponseRedirect(reverse('bom:home'))
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1223
|
+
def add_subpart(request, part_id, part_revision_id):
|
|
1224
|
+
user = request.user
|
|
1225
|
+
profile = user.bom_profile()
|
|
1226
|
+
organization = profile.organization
|
|
1227
|
+
|
|
1228
|
+
part_revision = get_object_or_404(PartRevision, pk=part_revision_id)
|
|
1229
|
+
|
|
1230
|
+
if request.method == 'POST':
|
|
1231
|
+
add_subpart_form = AddSubpartForm(request.POST, organization=organization, part_id=part_id)
|
|
1232
|
+
if add_subpart_form.is_valid():
|
|
1233
|
+
subpart_part = add_subpart_form.subpart_part
|
|
1234
|
+
reference = add_subpart_form.cleaned_data['reference']
|
|
1235
|
+
dnl = add_subpart_form.cleaned_data['do_not_load']
|
|
1236
|
+
count = add_subpart_form.cleaned_data['count']
|
|
1237
|
+
|
|
1238
|
+
first_level_bom = part_revision.assembly.subparts.filter(part_revision=subpart_part, do_not_load=dnl)
|
|
1239
|
+
|
|
1240
|
+
if first_level_bom.count() > 0:
|
|
1241
|
+
new_part = first_level_bom[0]
|
|
1242
|
+
new_part.count += count
|
|
1243
|
+
if reference:
|
|
1244
|
+
new_part.reference = new_part.reference + ', ' + reference
|
|
1245
|
+
new_part.save()
|
|
1246
|
+
else:
|
|
1247
|
+
new_part = Subpart.objects.create(
|
|
1248
|
+
part_revision=subpart_part,
|
|
1249
|
+
count=count,
|
|
1250
|
+
reference=reference,
|
|
1251
|
+
do_not_load=dnl)
|
|
1252
|
+
|
|
1253
|
+
if part_revision.assembly is None:
|
|
1254
|
+
part_revision.assembly = Assembly.objects.create()
|
|
1255
|
+
part_revision.save()
|
|
1256
|
+
|
|
1257
|
+
AssemblySubparts.objects.create(assembly=part_revision.assembly, subpart=new_part)
|
|
1258
|
+
|
|
1259
|
+
info_msg = "Added subpart "
|
|
1260
|
+
if reference:
|
|
1261
|
+
info_msg += ' ' + reference
|
|
1262
|
+
info_msg += " {} to part {}".format(subpart_part, part_revision)
|
|
1263
|
+
messages.info(request, info_msg)
|
|
1264
|
+
|
|
1265
|
+
else:
|
|
1266
|
+
messages.error(request, add_subpart_form.errors)
|
|
1267
|
+
|
|
1268
|
+
return HttpResponseRedirect(reverse('bom:part-manage-bom', kwargs={'part_id': part_id, 'part_revision_id': part_revision_id}))
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1272
|
+
@organization_admin
|
|
1273
|
+
def remove_subpart(request, part_id, part_revision_id, subpart_id):
|
|
1274
|
+
subpart = get_object_or_404(Subpart, pk=subpart_id)
|
|
1275
|
+
subpart.delete()
|
|
1276
|
+
return HttpResponseRedirect(
|
|
1277
|
+
reverse('bom:part-manage-bom', kwargs={'part_id': part_id, 'part_revision_id': part_revision_id}))
|
|
1278
|
+
|
|
1279
|
+
|
|
1280
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1281
|
+
def property_definition_edit(request, property_definition_id=None):
|
|
1282
|
+
user = request.user
|
|
1283
|
+
profile = user.bom_profile()
|
|
1284
|
+
organization = profile.organization
|
|
1285
|
+
|
|
1286
|
+
if property_definition_id:
|
|
1287
|
+
property_definition = get_object_or_404(PartRevisionPropertyDefinition, pk=property_definition_id)
|
|
1288
|
+
if property_definition.organization != organization:
|
|
1289
|
+
messages.error(request, "Can't access a property definition that is not yours!")
|
|
1290
|
+
return HttpResponseRedirect(reverse('bom:settings', kwargs={'tab_anchor': 'indabom'}))
|
|
1291
|
+
title = f'Edit Property Definition {property_definition.name}'
|
|
1292
|
+
else:
|
|
1293
|
+
property_definition = None
|
|
1294
|
+
title = 'Add Property Definition'
|
|
1295
|
+
|
|
1296
|
+
if request.method == 'POST':
|
|
1297
|
+
form = PartRevisionPropertyDefinitionForm(request.POST, instance=property_definition, organization=organization)
|
|
1298
|
+
if form.is_valid():
|
|
1299
|
+
property_definition = form.save(commit=False)
|
|
1300
|
+
property_definition.organization = organization
|
|
1301
|
+
property_definition.save()
|
|
1302
|
+
return HttpResponseRedirect(reverse('bom:settings', kwargs={'tab_anchor': 'indabom'}))
|
|
1303
|
+
else:
|
|
1304
|
+
form = PartRevisionPropertyDefinitionForm(instance=property_definition, organization=organization)
|
|
1305
|
+
|
|
1306
|
+
return TemplateResponse(request, 'bom/bom-form.html', locals())
|
|
1307
|
+
|
|
1308
|
+
|
|
1309
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1310
|
+
@organization_admin
|
|
1311
|
+
def property_definition_delete(request, property_definition_id):
|
|
1312
|
+
user = request.user
|
|
1313
|
+
profile = user.bom_profile()
|
|
1314
|
+
organization = profile.organization
|
|
1315
|
+
property_definition = get_object_or_404(PartRevisionPropertyDefinition, pk=property_definition_id)
|
|
1316
|
+
if property_definition.organization != organization:
|
|
1317
|
+
messages.error(request, "Can't delete a property definition that is not yours!")
|
|
1318
|
+
else:
|
|
1319
|
+
property_definition.delete()
|
|
1320
|
+
return HttpResponseRedirect(reverse('bom:settings', kwargs={'tab_anchor': 'indabom'}))
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1324
|
+
def quantity_of_measure_edit(request, quantity_of_measure_id=None):
|
|
1325
|
+
user = request.user
|
|
1326
|
+
profile = user.bom_profile()
|
|
1327
|
+
organization = profile.organization
|
|
1328
|
+
|
|
1329
|
+
if quantity_of_measure_id:
|
|
1330
|
+
quantity_of_measure = get_object_or_404(QuantityOfMeasure, pk=quantity_of_measure_id)
|
|
1331
|
+
if quantity_of_measure.organization != organization:
|
|
1332
|
+
messages.error(request, "Can't access a quantity of measure that is not yours!")
|
|
1333
|
+
return HttpResponseRedirect(reverse('bom:settings', kwargs={'tab_anchor': 'indabom'}))
|
|
1334
|
+
title = f'Edit Quantity of Measure {quantity_of_measure.name}'
|
|
1335
|
+
else:
|
|
1336
|
+
quantity_of_measure = None
|
|
1337
|
+
title = 'Add Quantity of Measure'
|
|
1338
|
+
|
|
1339
|
+
if request.method == 'POST':
|
|
1340
|
+
form = QuantityOfMeasureForm(request.POST, instance=quantity_of_measure, organization=organization)
|
|
1341
|
+
unit_formset = UnitDefinitionFormSet(
|
|
1342
|
+
request.POST,
|
|
1343
|
+
queryset=quantity_of_measure.units.all() if quantity_of_measure else UnitDefinition.objects.none(),
|
|
1344
|
+
form_kwargs={'organization': organization},
|
|
1345
|
+
prefix='units'
|
|
1346
|
+
)
|
|
1347
|
+
|
|
1348
|
+
if form.is_valid() and unit_formset.is_valid():
|
|
1349
|
+
quantity_of_measure = form.save()
|
|
1350
|
+
units = unit_formset.save(commit=False)
|
|
1351
|
+
base_multipliers = [float(unit.base_multiplier) for unit in units]
|
|
1352
|
+
if 1.0 not in base_multipliers:
|
|
1353
|
+
messages.error(request, "Must have a base multiplier of 1.0 for at least 1 unit.")
|
|
1354
|
+
return TemplateResponse(request, 'bom/edit-quantity-of-measure.html', locals())
|
|
1355
|
+
|
|
1356
|
+
for unit in units:
|
|
1357
|
+
unit.organization = organization
|
|
1358
|
+
unit.quantity_of_measure = quantity_of_measure
|
|
1359
|
+
unit.save()
|
|
1360
|
+
for obj in unit_formset.deleted_objects:
|
|
1361
|
+
obj.delete()
|
|
1362
|
+
return HttpResponseRedirect(reverse('bom:settings', kwargs={'tab_anchor': 'indabom'}))
|
|
1363
|
+
else:
|
|
1364
|
+
messages.error(request, form.errors)
|
|
1365
|
+
messages.error(request, unit_formset.errors)
|
|
1366
|
+
else:
|
|
1367
|
+
form = QuantityOfMeasureForm(instance=quantity_of_measure, organization=organization)
|
|
1368
|
+
unit_formset = UnitDefinitionFormSet(
|
|
1369
|
+
queryset=quantity_of_measure.units.all() if quantity_of_measure else UnitDefinition.objects.none(),
|
|
1370
|
+
form_kwargs={'organization': organization},
|
|
1371
|
+
prefix='units'
|
|
1372
|
+
)
|
|
1373
|
+
|
|
1374
|
+
return TemplateResponse(request, 'bom/edit-quantity-of-measure.html', locals())
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1378
|
+
@organization_admin
|
|
1379
|
+
def quantity_of_measure_delete(request, quantity_of_measure_id):
|
|
1380
|
+
user = request.user
|
|
1381
|
+
profile = user.bom_profile()
|
|
1382
|
+
organization = profile.organization
|
|
1383
|
+
quantity_of_measure = get_object_or_404(QuantityOfMeasure, pk=quantity_of_measure_id)
|
|
1384
|
+
if quantity_of_measure.organization != organization:
|
|
1385
|
+
messages.error(request, "Can't delete a quantity of measure that is not yours!")
|
|
1386
|
+
else:
|
|
1387
|
+
quantity_of_measure.delete()
|
|
1388
|
+
return HttpResponseRedirect(reverse('bom:settings', kwargs={'tab_anchor': 'indabom'}))
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1392
|
+
def part_class_edit(request, part_class_id):
|
|
1393
|
+
user = request.user
|
|
1394
|
+
profile = user.bom_profile()
|
|
1395
|
+
organization = profile.organization
|
|
1396
|
+
|
|
1397
|
+
part_class = get_object_or_404(PartClass, pk=part_class_id)
|
|
1398
|
+
title = 'Edit Part Class {}'.format(part_class.__str__())
|
|
1399
|
+
|
|
1400
|
+
if request.method == 'POST':
|
|
1401
|
+
part_class_form = PartClassForm(request.POST, instance=part_class, organization=organization)
|
|
1402
|
+
property_definitions_formset = PartRevisionPropertyDefinitionFormSet(
|
|
1403
|
+
request.POST,
|
|
1404
|
+
form_kwargs={'organization': organization},
|
|
1405
|
+
prefix='prop-def'
|
|
1406
|
+
)
|
|
1407
|
+
|
|
1408
|
+
if part_class_form.is_valid() and property_definitions_formset.is_valid():
|
|
1409
|
+
part_class = part_class_form.save()
|
|
1410
|
+
|
|
1411
|
+
# Clear current associations and re-add from formset
|
|
1412
|
+
part_class.property_definitions.clear()
|
|
1413
|
+
for form in property_definitions_formset:
|
|
1414
|
+
if form.cleaned_data and not form.cleaned_data.get('DELETE'):
|
|
1415
|
+
definition = form.cleaned_data.get('property_definition')
|
|
1416
|
+
if definition:
|
|
1417
|
+
part_class.property_definitions.add(definition)
|
|
1418
|
+
|
|
1419
|
+
return HttpResponseRedirect(reverse('bom:settings', kwargs={'tab_anchor': 'indabom'}))
|
|
1420
|
+
|
|
1421
|
+
else:
|
|
1422
|
+
if not part_class_form.is_valid():
|
|
1423
|
+
messages.error(request, part_class_form.errors)
|
|
1424
|
+
if not property_definitions_formset.is_valid():
|
|
1425
|
+
messages.error(request, property_definitions_formset.errors)
|
|
1426
|
+
return TemplateResponse(request, 'bom/edit-part-class.html', locals())
|
|
1427
|
+
|
|
1428
|
+
else:
|
|
1429
|
+
part_class_form = PartClassForm(instance=part_class, organization=organization)
|
|
1430
|
+
initial = [{'property_definition': pd} for pd in part_class.property_definitions.all().order_by('name')]
|
|
1431
|
+
property_definitions_formset = PartRevisionPropertyDefinitionFormSet(
|
|
1432
|
+
initial=initial,
|
|
1433
|
+
form_kwargs={'organization': organization},
|
|
1434
|
+
prefix='prop-def',
|
|
1435
|
+
)
|
|
1436
|
+
|
|
1437
|
+
return TemplateResponse(request, 'bom/edit-part-class.html', locals())
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1441
|
+
def edit_subpart(request, part_id, part_revision_id, subpart_id):
|
|
1442
|
+
user = request.user
|
|
1443
|
+
profile = user.bom_profile()
|
|
1444
|
+
organization = profile.organization
|
|
1445
|
+
action = reverse('bom:part-edit-subpart', kwargs={'part_id': part_id, 'subpart_id': subpart_id, 'part_revision_id': part_revision_id})
|
|
1446
|
+
|
|
1447
|
+
part = get_object_or_404(Part, pk=part_id)
|
|
1448
|
+
subpart = get_object_or_404(Subpart, pk=subpart_id)
|
|
1449
|
+
title = "Edit Subpart"
|
|
1450
|
+
h1 = "{} {}".format(subpart.part_revision.part.full_part_number(), subpart.part_revision.synopsis())
|
|
1451
|
+
|
|
1452
|
+
if request.method == 'POST':
|
|
1453
|
+
form = SubpartForm(request.POST, instance=subpart, organization=organization, part_id=subpart.part_revision.part.id)
|
|
1454
|
+
if form.is_valid():
|
|
1455
|
+
reference_list = listify_string(form.cleaned_data['reference'])
|
|
1456
|
+
count = form.cleaned_data['count']
|
|
1457
|
+
form.save()
|
|
1458
|
+
return HttpResponseRedirect(reverse('bom:part-manage-bom', kwargs={'part_id': part_id, 'part_revision_id': part_revision_id}))
|
|
1459
|
+
else:
|
|
1460
|
+
return TemplateResponse(request, 'bom/bom-form.html', locals())
|
|
1461
|
+
|
|
1462
|
+
else:
|
|
1463
|
+
form = SubpartForm(instance=subpart, organization=organization, part_id=subpart.part_revision.part.id)
|
|
1464
|
+
|
|
1465
|
+
return TemplateResponse(request, 'bom/bom-form.html', locals())
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1469
|
+
@organization_admin
|
|
1470
|
+
def remove_all_subparts(request, part_id, part_revision_id):
|
|
1471
|
+
part_revision = get_object_or_404(PartRevision, pk=part_revision_id)
|
|
1472
|
+
part_revision.assembly.subparts.all().delete()
|
|
1473
|
+
return HttpResponseRedirect(reverse('bom:part-manage-bom', kwargs={'part_id': part_id, 'part_revision_id': part_revision_id}))
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1477
|
+
def add_sellerpart(request, manufacturer_part_id):
|
|
1478
|
+
user = request.user
|
|
1479
|
+
profile = user.bom_profile()
|
|
1480
|
+
organization = profile.organization
|
|
1481
|
+
title = 'Add Seller Part'
|
|
1482
|
+
|
|
1483
|
+
manufacturer_part = get_object_or_404(ManufacturerPart, pk=manufacturer_part_id)
|
|
1484
|
+
title = "Add Seller Part to {}".format(manufacturer_part)
|
|
1485
|
+
|
|
1486
|
+
if request.method == 'POST':
|
|
1487
|
+
form = SellerPartForm(request.POST, manufacturer_part=manufacturer_part, organization=organization)
|
|
1488
|
+
if form.is_valid():
|
|
1489
|
+
form.save()
|
|
1490
|
+
return HttpResponseRedirect(
|
|
1491
|
+
reverse('bom:part-info', kwargs={'part_id': manufacturer_part.part.id}) + '?tab_anchor=sourcing')
|
|
1492
|
+
else:
|
|
1493
|
+
form = SellerPartForm(organization=organization)
|
|
1494
|
+
|
|
1495
|
+
return TemplateResponse(request, 'bom/bom-form.html', locals())
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1499
|
+
def add_manufacturer_part(request, part_id):
|
|
1500
|
+
user = request.user
|
|
1501
|
+
profile = user.bom_profile()
|
|
1502
|
+
organization = profile.organization
|
|
1503
|
+
|
|
1504
|
+
part = get_object_or_404(Part, pk=part_id)
|
|
1505
|
+
title = 'Add Manufacturer Part to {}'.format(part.full_part_number())
|
|
1506
|
+
|
|
1507
|
+
if request.method == 'POST':
|
|
1508
|
+
manufacturer_form = ManufacturerForm(request.POST)
|
|
1509
|
+
manufacturer_part_form = ManufacturerPartForm(request.POST, organization=organization)
|
|
1510
|
+
if manufacturer_form.is_valid() and manufacturer_part_form.is_valid():
|
|
1511
|
+
manufacturer_part_number = manufacturer_part_form.cleaned_data['manufacturer_part_number']
|
|
1512
|
+
manufacturer = manufacturer_part_form.cleaned_data['manufacturer']
|
|
1513
|
+
new_manufacturer_name = manufacturer_form.cleaned_data['name']
|
|
1514
|
+
if manufacturer is None and new_manufacturer_name == '':
|
|
1515
|
+
messages.error(request, "Must either select an existing manufacturer, or enter a new manufacturer name.")
|
|
1516
|
+
return TemplateResponse(request, 'bom/add-manufacturer-part.html', locals())
|
|
1517
|
+
|
|
1518
|
+
if new_manufacturer_name != '' and new_manufacturer_name is not None:
|
|
1519
|
+
manufacturer, created = Manufacturer.objects.get_or_create(name__iexact=new_manufacturer_name, organization=organization, defaults={'name': new_manufacturer_name})
|
|
1520
|
+
manufacturer_part_form.cleaned_data['manufacturer'] = manufacturer
|
|
1521
|
+
|
|
1522
|
+
manufacturer_part, created = ManufacturerPart.objects.get_or_create(part=part, manufacturer_part_number=manufacturer_part_number, manufacturer=manufacturer)
|
|
1523
|
+
|
|
1524
|
+
if part.primary_manufacturer_part is None and manufacturer_part is not None:
|
|
1525
|
+
part.primary_manufacturer_part = manufacturer_part
|
|
1526
|
+
part.save()
|
|
1527
|
+
|
|
1528
|
+
return HttpResponseRedirect(
|
|
1529
|
+
reverse('bom:part-info', kwargs={'part_id': str(part.id)}) + '?tab_anchor=sourcing')
|
|
1530
|
+
else:
|
|
1531
|
+
messages.error(request, "{}".format(manufacturer_form.is_valid()))
|
|
1532
|
+
messages.error(request, "{}".format(manufacturer_part_form.is_valid()))
|
|
1533
|
+
else:
|
|
1534
|
+
default_mfg = Manufacturer.objects.filter(organization=organization, name__iexact=organization.name).first()
|
|
1535
|
+
manufacturer_form = ManufacturerForm(initial={'organization': organization})
|
|
1536
|
+
manufacturer_part_form = ManufacturerPartForm(organization=organization, initial={'manufacturer_part_number': part.full_part_number(), 'manufacturer': default_mfg})
|
|
1537
|
+
|
|
1538
|
+
return TemplateResponse(request, 'bom/add-manufacturer-part.html', locals())
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1542
|
+
def manufacturer_part_edit(request, manufacturer_part_id):
|
|
1543
|
+
user = request.user
|
|
1544
|
+
profile = user.bom_profile()
|
|
1545
|
+
organization = profile.organization
|
|
1546
|
+
|
|
1547
|
+
title = 'Edit Manufacturer Part'
|
|
1548
|
+
|
|
1549
|
+
manufacturer_part = get_object_or_404(ManufacturerPart, pk=manufacturer_part_id)
|
|
1550
|
+
part = manufacturer_part.part
|
|
1551
|
+
|
|
1552
|
+
if request.method == 'POST':
|
|
1553
|
+
manufacturer_part_form = ManufacturerPartForm(request.POST, instance=manufacturer_part, organization=organization)
|
|
1554
|
+
manufacturer_form = ManufacturerForm(request.POST, instance=manufacturer_part.manufacturer)
|
|
1555
|
+
if manufacturer_part_form.is_valid() and manufacturer_form.is_valid():
|
|
1556
|
+
manufacturer_part_number = manufacturer_part_form.cleaned_data.get('manufacturer_part_number')
|
|
1557
|
+
manufacturer = manufacturer_part_form.cleaned_data.get('manufacturer', None)
|
|
1558
|
+
new_manufacturer_name = manufacturer_form.cleaned_data.get('name', '')
|
|
1559
|
+
|
|
1560
|
+
if manufacturer is None and new_manufacturer_name == '':
|
|
1561
|
+
messages.error(request, "Must either select an existing manufacturer, or enter a new manufacturer name.")
|
|
1562
|
+
return TemplateResponse(request, 'bom/edit-manufacturer-part.html', locals())
|
|
1563
|
+
|
|
1564
|
+
new_manufacturer = None
|
|
1565
|
+
if new_manufacturer_name != '' and new_manufacturer_name is not None:
|
|
1566
|
+
new_manufacturer, created = Manufacturer.objects.get_or_create(name__iexact=new_manufacturer_name, organization=organization, defaults={'name': new_manufacturer_name})
|
|
1567
|
+
manufacturer_part = manufacturer_part_form.save(commit=False)
|
|
1568
|
+
manufacturer_part.manufacturer = new_manufacturer
|
|
1569
|
+
manufacturer_part.save()
|
|
1570
|
+
else:
|
|
1571
|
+
manufacturer_part = manufacturer_part_form.save()
|
|
1572
|
+
|
|
1573
|
+
if part.primary_manufacturer_part is None and manufacturer_part is not None:
|
|
1574
|
+
part.primary_manufacturer_part = manufacturer_part
|
|
1575
|
+
part.save()
|
|
1576
|
+
return HttpResponseRedirect(
|
|
1577
|
+
reverse('bom:part-info', kwargs={'part_id': manufacturer_part.part.id}) + '?tab_anchor=sourcing')
|
|
1578
|
+
else:
|
|
1579
|
+
messages.error(request, manufacturer_part_form.errors)
|
|
1580
|
+
messages.error(request, manufacturer_form.errors)
|
|
1581
|
+
else:
|
|
1582
|
+
if manufacturer_part.manufacturer is None:
|
|
1583
|
+
manufacturer_form = ManufacturerForm(instance=manufacturer_part.manufacturer, initial={'organization': organization})
|
|
1584
|
+
else:
|
|
1585
|
+
manufacturer_form = ManufacturerForm(initial={'organization': organization})
|
|
1586
|
+
|
|
1587
|
+
manufacturer_part_form = ManufacturerPartForm(instance=manufacturer_part, organization=organization, )
|
|
1588
|
+
|
|
1589
|
+
return TemplateResponse(request, 'bom/edit-manufacturer-part.html', locals())
|
|
1590
|
+
|
|
1591
|
+
|
|
1592
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1593
|
+
@organization_admin
|
|
1594
|
+
def manufacturer_part_delete(request, manufacturer_part_id):
|
|
1595
|
+
manufacturer_part = get_object_or_404(ManufacturerPart, pk=manufacturer_part_id)
|
|
1596
|
+
part = manufacturer_part.part
|
|
1597
|
+
manufacturer_part.delete()
|
|
1598
|
+
return HttpResponseRedirect(reverse('bom:part-info', kwargs={'part_id': part.id}) + '?tab_anchor=sourcing')
|
|
1599
|
+
|
|
1600
|
+
|
|
1601
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1602
|
+
def sellerpart_edit(request, sellerpart_id):
|
|
1603
|
+
user = request.user
|
|
1604
|
+
profile = user.bom_profile()
|
|
1605
|
+
organization = profile.organization
|
|
1606
|
+
|
|
1607
|
+
title = "Edit Seller Part"
|
|
1608
|
+
action = reverse('bom:sellerpart-edit', kwargs={'sellerpart_id': sellerpart_id})
|
|
1609
|
+
sellerpart = get_object_or_404(SellerPart, pk=sellerpart_id)
|
|
1610
|
+
|
|
1611
|
+
if request.method == 'POST':
|
|
1612
|
+
form = SellerPartForm(request.POST, instance=sellerpart, organization=organization)
|
|
1613
|
+
if form.is_valid():
|
|
1614
|
+
form.save()
|
|
1615
|
+
return HttpResponseRedirect(reverse('bom:part-info', kwargs={'part_id': sellerpart.manufacturer_part.part.id}) + '?tab_anchor=sourcing')
|
|
1616
|
+
else:
|
|
1617
|
+
form = SellerPartForm(instance=sellerpart, organization=organization)
|
|
1618
|
+
|
|
1619
|
+
return TemplateResponse(request, 'bom/bom-form.html', locals())
|
|
1620
|
+
|
|
1621
|
+
|
|
1622
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1623
|
+
@organization_admin
|
|
1624
|
+
def sellerpart_delete(request, sellerpart_id):
|
|
1625
|
+
sellerpart = get_object_or_404(SellerPart, pk=sellerpart_id)
|
|
1626
|
+
part = sellerpart.manufacturer_part.part
|
|
1627
|
+
sellerpart.delete()
|
|
1628
|
+
return HttpResponseRedirect(reverse('bom:part-info', kwargs={'part_id': part.id}) + '?tab_anchor=sourcing')
|
|
1629
|
+
|
|
1630
|
+
|
|
1631
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1632
|
+
def part_revision_release(request, part_id, part_revision_id):
|
|
1633
|
+
user = request.user
|
|
1634
|
+
profile = user.bom_profile()
|
|
1635
|
+
organization = profile.organization
|
|
1636
|
+
|
|
1637
|
+
part = get_object_or_404(Part, pk=part_id)
|
|
1638
|
+
part_revision = get_object_or_404(PartRevision, pk=part_revision_id)
|
|
1639
|
+
action = reverse('bom:part-revision-release', kwargs={'part_id': part.id, 'part_revision_id': part_revision.id})
|
|
1640
|
+
title = 'Promote {} Rev {} {} from <b>Working</b> to <b>Released</b>?'.format(part.full_part_number(), part_revision.revision, part_revision.synopsis())
|
|
1641
|
+
|
|
1642
|
+
subparts = part_revision.assembly.subparts.filter(part_revision__configuration="W")
|
|
1643
|
+
release_warning = subparts.count() > 0
|
|
1644
|
+
|
|
1645
|
+
if request.method == 'POST':
|
|
1646
|
+
part_revision.configuration = 'R'
|
|
1647
|
+
part_revision.save()
|
|
1648
|
+
return HttpResponseRedirect(reverse('bom:part-info-history', kwargs={'part_id': part.id, 'part_revision_id': part_revision.id}))
|
|
1649
|
+
|
|
1650
|
+
return TemplateResponse(request, 'bom/part-revision-release.html', locals())
|
|
1651
|
+
|
|
1652
|
+
|
|
1653
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1654
|
+
def part_revision_revert(request, part_id, part_revision_id):
|
|
1655
|
+
user = request.user
|
|
1656
|
+
profile = user.bom_profile()
|
|
1657
|
+
organization = profile.organization
|
|
1658
|
+
|
|
1659
|
+
part_revision = get_object_or_404(PartRevision, pk=part_revision_id)
|
|
1660
|
+
part_revision.configuration = 'W'
|
|
1661
|
+
part_revision.save()
|
|
1662
|
+
return HttpResponseRedirect(reverse('bom:part-info-history', kwargs={'part_id': part_id, 'part_revision_id': part_revision_id}))
|
|
1663
|
+
|
|
1664
|
+
|
|
1665
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1666
|
+
def part_revision_new(request, part_id):
|
|
1667
|
+
user = request.user
|
|
1668
|
+
profile = user.bom_profile()
|
|
1669
|
+
organization = profile.organization
|
|
1670
|
+
|
|
1671
|
+
part = get_object_or_404(Part, pk=part_id)
|
|
1672
|
+
title = 'New Revision for {}'.format(part.full_part_number())
|
|
1673
|
+
action = reverse('bom:part-revision-new', kwargs={'part_id': part_id})
|
|
1674
|
+
|
|
1675
|
+
latest_revision = part.latest()
|
|
1676
|
+
next_revision_number = latest_revision.next_revision() if latest_revision else None
|
|
1677
|
+
|
|
1678
|
+
all_part_revisions = part.revisions()
|
|
1679
|
+
all_used_part_revisions = PartRevision.objects.filter(part=part)
|
|
1680
|
+
used_in_subparts = Subpart.objects.filter(part_revision__in=all_used_part_revisions)
|
|
1681
|
+
used_in_assembly_ids = AssemblySubparts.objects.filter(subpart__in=used_in_subparts).values_list('assembly', flat=True)
|
|
1682
|
+
all_used_in_prs = PartRevision.objects.filter(assembly__in=used_in_assembly_ids)
|
|
1683
|
+
used_part_revisions = all_used_in_prs.filter(configuration='W')
|
|
1684
|
+
|
|
1685
|
+
if request.method == 'POST':
|
|
1686
|
+
part_revision_new_form = PartRevisionNewForm(request.POST, part=part, revision=next_revision_number,
|
|
1687
|
+
assembly=latest_revision.assembly, organization=organization)
|
|
1688
|
+
if part_revision_new_form.is_valid():
|
|
1689
|
+
new_part_revision = part_revision_new_form.save()
|
|
1690
|
+
|
|
1691
|
+
revisions_to_roll = request.POST.getlist('roll')
|
|
1692
|
+
# TODO: could optimize this, but probably shouldn't get too crazy so may be fine...
|
|
1693
|
+
for r_id in revisions_to_roll:
|
|
1694
|
+
subparts = PartRevision.objects.get(id=r_id).assembly.subparts \
|
|
1695
|
+
.filter(part_revision__in=all_part_revisions)
|
|
1696
|
+
subparts.update(part_revision=new_part_revision)
|
|
1697
|
+
|
|
1698
|
+
if part_revision_new_form.cleaned_data['copy_assembly']:
|
|
1699
|
+
old_subparts = latest_revision.assembly.subparts.all() if latest_revision.assembly is not None else None
|
|
1700
|
+
new_assembly = latest_revision.assembly if latest_revision.assembly is not None else Assembly()
|
|
1701
|
+
new_assembly.pk = None
|
|
1702
|
+
new_assembly.save()
|
|
1703
|
+
|
|
1704
|
+
part_revision_new_form.cleaned_data['assembly'] = new_assembly
|
|
1705
|
+
|
|
1706
|
+
new_part_revision.assembly = new_assembly
|
|
1707
|
+
new_part_revision.save()
|
|
1708
|
+
for sp in old_subparts:
|
|
1709
|
+
new_sp = sp
|
|
1710
|
+
new_sp.pk = None
|
|
1711
|
+
new_sp.save()
|
|
1712
|
+
AssemblySubparts.objects.create(assembly=new_assembly, subpart=new_sp)
|
|
1713
|
+
return HttpResponseRedirect(reverse('bom:part-info', kwargs={'part_id': part_id}))
|
|
1714
|
+
|
|
1715
|
+
else:
|
|
1716
|
+
if latest_revision:
|
|
1717
|
+
messages.info(request, 'New revision automatically incremented to `{}` from your last revision `{}`.'.format(next_revision_number, latest_revision.revision))
|
|
1718
|
+
latest_revision.revision = next_revision_number # use updated object to populate form but don't save changes
|
|
1719
|
+
part_revision_new_form = PartRevisionNewForm(instance=latest_revision, organization=organization)
|
|
1720
|
+
else:
|
|
1721
|
+
part_revision_new_form = PartRevisionNewForm(organization=organization)
|
|
1722
|
+
|
|
1723
|
+
return TemplateResponse(request, 'bom/part-revision-new.html', locals())
|
|
1724
|
+
|
|
1725
|
+
|
|
1726
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1727
|
+
def part_revision_edit(request, part_id, part_revision_id):
|
|
1728
|
+
user = request.user
|
|
1729
|
+
profile = user.bom_profile()
|
|
1730
|
+
organization = profile.organization
|
|
1731
|
+
|
|
1732
|
+
part = get_object_or_404(Part, pk=part_id)
|
|
1733
|
+
part_revision = get_object_or_404(PartRevision, pk=part_revision_id)
|
|
1734
|
+
title = 'Edit {} Rev {}'.format(part.full_part_number(), part_revision.revision)
|
|
1735
|
+
|
|
1736
|
+
action = reverse('bom:part-revision-edit', kwargs={'part_id': part_id, 'part_revision_id': part_revision_id})
|
|
1737
|
+
|
|
1738
|
+
if request.method == 'POST':
|
|
1739
|
+
form = PartRevisionForm(request.POST, instance=part_revision, organization=organization)
|
|
1740
|
+
if form.is_valid():
|
|
1741
|
+
form.save()
|
|
1742
|
+
return HttpResponseRedirect(reverse('bom:part-info', kwargs={'part_id': part_id}))
|
|
1743
|
+
else:
|
|
1744
|
+
form = PartRevisionForm(instance=part_revision, organization=organization)
|
|
1745
|
+
|
|
1746
|
+
return TemplateResponse(request, 'bom/part-revision-edit.html', locals())
|
|
1747
|
+
|
|
1748
|
+
|
|
1749
|
+
@login_required(login_url=BOM_LOGIN_URL)
|
|
1750
|
+
@organization_admin
|
|
1751
|
+
def part_revision_delete(request, part_id, part_revision_id):
|
|
1752
|
+
part_revision = get_object_or_404(PartRevision, pk=part_revision_id)
|
|
1753
|
+
part = part_revision.part
|
|
1754
|
+
part_revision.delete()
|
|
1755
|
+
|
|
1756
|
+
if part.revisions().count() == 0:
|
|
1757
|
+
part.delete()
|
|
1758
|
+
messages.info(request, 'Deleted {}.'.format(part.full_part_number()))
|
|
1759
|
+
return HttpResponseRedirect(reverse('bom:home'))
|
|
1760
|
+
|
|
1761
|
+
messages.info(request, 'Deleted {} Rev {}.'.format(part.full_part_number(), part_revision.revision))
|
|
1762
|
+
|
|
1763
|
+
return HttpResponseRedirect(reverse('bom:part-info', kwargs={'part_id': part.id}))
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
class Help(TemplateView):
|
|
1767
|
+
name = 'help'
|
|
1768
|
+
template_name = f'bom/{name}.html'
|
|
1769
|
+
|
|
1770
|
+
def get_context_data(self, *args, **kwargs):
|
|
1771
|
+
context = super(Help, self).get_context_data(**kwargs)
|
|
1772
|
+
context['name'] = self.name
|
|
1773
|
+
return context
|