django-bom 1.235__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 +161 -0
- bom/apps.py +8 -0
- bom/base_classes.py +31 -0
- bom/constants.py +210 -0
- bom/context_processors.py +9 -0
- bom/csv_headers.py +274 -0
- bom/decorators.py +32 -0
- bom/form_fields.py +59 -0
- bom/forms.py +1400 -0
- bom/helpers.py +292 -0
- bom/local_settings.py +36 -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/__init__.py +0 -0
- bom/models.py +756 -0
- bom/part_bom.py +192 -0
- bom/settings.py +225 -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 +308 -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/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 +69 -0
- bom/templates/bom/add-sellerpart.html +96 -0
- bom/templates/bom/base-menu.html +7 -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 +35 -0
- bom/templates/bom/bom-form.html +34 -0
- bom/templates/bom/bom-signup.html +16 -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 +271 -0
- bom/templates/bom/components/seller-part-list.html +63 -0
- bom/templates/bom/create-part.html +68 -0
- bom/templates/bom/dashboard-menu.html +15 -0
- bom/templates/bom/dashboard.html +300 -0
- bom/templates/bom/edit-manufacturer-part.html +75 -0
- bom/templates/bom/edit-part-class.html +36 -0
- bom/templates/bom/edit-part.html +71 -0
- bom/templates/bom/edit-user-meta.html +41 -0
- bom/templates/bom/help.html +1363 -0
- bom/templates/bom/manufacturer-info.html +83 -0
- bom/templates/bom/manufacturers.html +96 -0
- bom/templates/bom/nothing-to-see.html +15 -0
- bom/templates/bom/organization-create.html +135 -0
- bom/templates/bom/part-info.html +440 -0
- bom/templates/bom/part-revision-display.html +184 -0
- bom/templates/bom/part-revision-edit.html +42 -0
- bom/templates/bom/part-revision-manage-bom.html +116 -0
- bom/templates/bom/part-revision-new.html +60 -0
- bom/templates/bom/part-revision-release.html +48 -0
- bom/templates/bom/search-help.html +105 -0
- bom/templates/bom/seller-info.html +83 -0
- bom/templates/bom/sellers.html +96 -0
- bom/templates/bom/settings.html +405 -0
- bom/templates/bom/signup.html +28 -0
- bom/templates/bom/table_of_contents.html +47 -0
- bom/templates/bom/upload-bom.html +112 -0
- bom/templates/bom/upload-parts-help.html +107 -0
- bom/templates/bom/upload-parts.html +54 -0
- bom/templates/registration/login.html +39 -0
- bom/tests.py +1506 -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 +89 -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 +1620 -0
- bom/wsgi.py +16 -0
- django_bom-1.235.dist-info/METADATA +207 -0
- django_bom-1.235.dist-info/RECORD +182 -0
- django_bom-1.235.dist-info/WHEEL +5 -0
- django_bom-1.235.dist-info/licenses/LICENSE +674 -0
- django_bom-1.235.dist-info/top_level.txt +1 -0
bom/forms.py
ADDED
|
@@ -0,0 +1,1400 @@
|
|
|
1
|
+
import codecs
|
|
2
|
+
import csv
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Type, TypeVar
|
|
5
|
+
|
|
6
|
+
from django import forms
|
|
7
|
+
from django.contrib.auth.forms import UserCreationForm
|
|
8
|
+
from django.core.exceptions import ValidationError
|
|
9
|
+
from django.core.validators import MaxLengthValidator, MaxValueValidator, MinLengthValidator, MinValueValidator
|
|
10
|
+
from django.db import IntegrityError
|
|
11
|
+
from django.forms.models import model_to_dict
|
|
12
|
+
from django.utils.translation import gettext_lazy as _
|
|
13
|
+
|
|
14
|
+
from djmoney.money import Money
|
|
15
|
+
|
|
16
|
+
from .constants import (
|
|
17
|
+
CONFIGURATION_TYPES,
|
|
18
|
+
CURRENT_UNITS,
|
|
19
|
+
DISTANCE_UNITS,
|
|
20
|
+
FREQUENCY_UNITS,
|
|
21
|
+
INTERFACE_TYPES,
|
|
22
|
+
MEMORY_UNITS,
|
|
23
|
+
NUMBER_CLASS_CODE_LEN_DEFAULT,
|
|
24
|
+
NUMBER_CLASS_CODE_LEN_MAX,
|
|
25
|
+
NUMBER_CLASS_CODE_LEN_MIN,
|
|
26
|
+
NUMBER_ITEM_LEN_DEFAULT,
|
|
27
|
+
NUMBER_ITEM_LEN_MAX,
|
|
28
|
+
NUMBER_ITEM_LEN_MIN,
|
|
29
|
+
NUMBER_SCHEME_INTELLIGENT,
|
|
30
|
+
NUMBER_SCHEME_SEMI_INTELLIGENT,
|
|
31
|
+
NUMBER_VARIATION_LEN_DEFAULT,
|
|
32
|
+
NUMBER_VARIATION_LEN_MAX,
|
|
33
|
+
NUMBER_VARIATION_LEN_MIN,
|
|
34
|
+
PACKAGE_TYPES,
|
|
35
|
+
POWER_UNITS,
|
|
36
|
+
ROLE_TYPE_VIEWER,
|
|
37
|
+
ROLE_TYPES,
|
|
38
|
+
SUBSCRIPTION_TYPES,
|
|
39
|
+
TEMPERATURE_UNITS,
|
|
40
|
+
VALUE_UNITS,
|
|
41
|
+
VOLTAGE_UNITS,
|
|
42
|
+
WAVELENGTH_UNITS,
|
|
43
|
+
WEIGHT_UNITS,
|
|
44
|
+
)
|
|
45
|
+
from .csv_headers import (
|
|
46
|
+
BOMFlatCSVHeaders,
|
|
47
|
+
BOMIndentedCSVHeaders,
|
|
48
|
+
CSVHeaderError,
|
|
49
|
+
PartClassesCSVHeaders,
|
|
50
|
+
PartsListCSVHeaders,
|
|
51
|
+
)
|
|
52
|
+
from .form_fields import AutocompleteTextInput
|
|
53
|
+
from .models import (
|
|
54
|
+
Assembly,
|
|
55
|
+
AssemblySubparts,
|
|
56
|
+
Manufacturer,
|
|
57
|
+
ManufacturerPart,
|
|
58
|
+
Organization,
|
|
59
|
+
Part,
|
|
60
|
+
PartClass,
|
|
61
|
+
PartRevision,
|
|
62
|
+
Seller,
|
|
63
|
+
SellerPart,
|
|
64
|
+
Subpart,
|
|
65
|
+
User,
|
|
66
|
+
UserMeta,
|
|
67
|
+
)
|
|
68
|
+
from .utils import (
|
|
69
|
+
check_references_for_duplicates,
|
|
70
|
+
get_from_dict,
|
|
71
|
+
listify_string,
|
|
72
|
+
prep_for_sorting_nicely,
|
|
73
|
+
stringify_list,
|
|
74
|
+
)
|
|
75
|
+
from .validators import alphanumeric, decimal, numeric
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
logger = logging.getLogger(__name__)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class UserModelChoiceField(forms.ModelChoiceField):
|
|
82
|
+
def label_from_instance(self, user):
|
|
83
|
+
l = "[" + user.username + "]"
|
|
84
|
+
if user.first_name:
|
|
85
|
+
l += " " + user.first_name
|
|
86
|
+
if user.last_name:
|
|
87
|
+
l += " " + user.last_name
|
|
88
|
+
if user.email:
|
|
89
|
+
l += ", " + user.email
|
|
90
|
+
return l
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class UserCreateForm(UserCreationForm):
|
|
94
|
+
first_name = forms.CharField(required=True)
|
|
95
|
+
last_name = forms.CharField(required=True)
|
|
96
|
+
email = forms.EmailField(required=True)
|
|
97
|
+
|
|
98
|
+
def clean_email(self):
|
|
99
|
+
email = self.cleaned_data['email']
|
|
100
|
+
exists = User.objects.filter(email__iexact=email).count() > 0
|
|
101
|
+
if exists:
|
|
102
|
+
raise ValidationError('An account with this email address already exists.')
|
|
103
|
+
return email
|
|
104
|
+
|
|
105
|
+
def save(self, commit=True):
|
|
106
|
+
user = super(UserCreateForm, self).save(commit=commit)
|
|
107
|
+
user.email = self.cleaned_data['email']
|
|
108
|
+
user.first_name = self.cleaned_data['first_name']
|
|
109
|
+
user.last_name = self.cleaned_data['last_name']
|
|
110
|
+
user.save()
|
|
111
|
+
return user
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class UserForm(forms.ModelForm):
|
|
115
|
+
class Meta:
|
|
116
|
+
model = User
|
|
117
|
+
fields = ['first_name', 'last_name', 'email', ]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class UserAddForm(forms.ModelForm):
|
|
121
|
+
class Meta:
|
|
122
|
+
model = UserMeta
|
|
123
|
+
fields = ['role']
|
|
124
|
+
|
|
125
|
+
field_order = ['username', 'role', ]
|
|
126
|
+
username = forms.CharField(initial=None, required=False)
|
|
127
|
+
|
|
128
|
+
def __init__(self, *args, **kwargs):
|
|
129
|
+
self.organization = kwargs.pop('organization', None)
|
|
130
|
+
super(UserAddForm, self).__init__(*args, **kwargs)
|
|
131
|
+
self.fields['role'].required = False
|
|
132
|
+
|
|
133
|
+
def clean_username(self):
|
|
134
|
+
cleaned_data = super(UserAddForm, self).clean()
|
|
135
|
+
username = cleaned_data.get('username')
|
|
136
|
+
try:
|
|
137
|
+
user = User.objects.get(username=username)
|
|
138
|
+
user_meta = UserMeta.objects.get(user=user)
|
|
139
|
+
if user_meta.organization == self.organization:
|
|
140
|
+
validation_error = forms.ValidationError("User '{0}' already belongs to {1}.".format(username, self.organization), code='invalid')
|
|
141
|
+
self.add_error('username', validation_error)
|
|
142
|
+
elif user_meta.organization:
|
|
143
|
+
validation_error = forms.ValidationError("User '{}' belongs to another organization.".format(username), code='invalid')
|
|
144
|
+
self.add_error('username', validation_error)
|
|
145
|
+
except User.DoesNotExist:
|
|
146
|
+
validation_error = forms.ValidationError("User '{}' does not exist.".format(username), code='invalid')
|
|
147
|
+
self.add_error('username', validation_error)
|
|
148
|
+
|
|
149
|
+
return username
|
|
150
|
+
|
|
151
|
+
def clean_role(self):
|
|
152
|
+
cleaned_data = super(UserAddForm, self).clean()
|
|
153
|
+
role = cleaned_data.get('role', None)
|
|
154
|
+
if not role:
|
|
155
|
+
role = ROLE_TYPE_VIEWER
|
|
156
|
+
return role
|
|
157
|
+
|
|
158
|
+
def save(self, *args, **kwargs):
|
|
159
|
+
username = self.cleaned_data.get('username')
|
|
160
|
+
role = self.cleaned_data.get('role', ROLE_TYPE_VIEWER)
|
|
161
|
+
user = User.objects.get(username=username)
|
|
162
|
+
user_meta = user.bom_profile()
|
|
163
|
+
user_meta.organization = self.organization
|
|
164
|
+
user_meta.role = role
|
|
165
|
+
user_meta.save()
|
|
166
|
+
return user_meta
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class UserMetaForm(forms.ModelForm):
|
|
170
|
+
class Meta:
|
|
171
|
+
model = UserMeta
|
|
172
|
+
exclude = ['user', ]
|
|
173
|
+
|
|
174
|
+
def __init__(self, *args, **kwargs):
|
|
175
|
+
self.organization = kwargs.pop('organization', None)
|
|
176
|
+
super(UserMetaForm, self).__init__(*args, **kwargs)
|
|
177
|
+
|
|
178
|
+
def save(self):
|
|
179
|
+
self.instance.organization = self.organization
|
|
180
|
+
self.instance.save()
|
|
181
|
+
return self.instance
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class OrganizationCreateForm(forms.ModelForm):
|
|
185
|
+
def __init__(self, *args, **kwargs):
|
|
186
|
+
super(OrganizationCreateForm, self).__init__(*args, **kwargs)
|
|
187
|
+
if self.data.get('number_scheme') == NUMBER_SCHEME_INTELLIGENT:
|
|
188
|
+
# make the QueryDict mutable
|
|
189
|
+
self.data = self.data.copy()
|
|
190
|
+
self.data['number_class_code_len'] = 3
|
|
191
|
+
self.data['number_item_len'] = 128
|
|
192
|
+
self.data['number_variation_len'] = 2
|
|
193
|
+
|
|
194
|
+
class Meta:
|
|
195
|
+
model = Organization
|
|
196
|
+
fields = ['name', 'number_scheme', 'number_class_code_len', 'number_item_len', 'number_variation_len', ]
|
|
197
|
+
labels = {
|
|
198
|
+
"name": "Organization Name",
|
|
199
|
+
"number_class_code_len": "Number Class Code Length (C)",
|
|
200
|
+
"number_item_len": "Number Item Length (N)",
|
|
201
|
+
"number_variation_len": "Number Variation Length (V)",
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class OrganizationForm(forms.ModelForm):
|
|
206
|
+
class Meta:
|
|
207
|
+
model = Organization
|
|
208
|
+
exclude = ['owner', 'subscription', 'subscription_quantity', 'google_drive_parent', 'number_scheme', ]
|
|
209
|
+
labels = {
|
|
210
|
+
"name": "Organization Name",
|
|
211
|
+
"number_class_code_len": "Number Class Code Length (C)",
|
|
212
|
+
"number_item_len": "Number Item Length (N)",
|
|
213
|
+
"number_variation_len": "Number Variation Length (V)",
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
def __init__(self, *args, **kwargs):
|
|
217
|
+
user = kwargs.pop('user', None)
|
|
218
|
+
super(OrganizationForm, self).__init__(*args, **kwargs)
|
|
219
|
+
if user and self.instance.owner == user:
|
|
220
|
+
user_queryset = User.objects.filter(
|
|
221
|
+
id__in=UserMeta.objects.filter(organization=self.instance, role='A').values_list('user', flat=True)).order_by(
|
|
222
|
+
'first_name', 'last_name', 'email')
|
|
223
|
+
self.fields['owner'] = UserModelChoiceField(queryset=user_queryset, label='Owner', initial=self.instance.owner, required=True)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class OrganizationFormEditSettings(OrganizationForm):
|
|
227
|
+
def __init__(self, *args, **kwargs):
|
|
228
|
+
super(OrganizationFormEditSettings, self).__init__(*args, **kwargs)
|
|
229
|
+
user = kwargs.get('user', None)
|
|
230
|
+
|
|
231
|
+
class Meta:
|
|
232
|
+
model = Organization
|
|
233
|
+
exclude = ['subscription', 'subscription_quantity', 'google_drive_parent', 'number_scheme', 'number_item_len', 'number_class_code_len', 'number_variation_len', 'owner', ]
|
|
234
|
+
labels = {
|
|
235
|
+
"name": "Organization Name",
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class OrganizationNumberLenForm(forms.ModelForm):
|
|
240
|
+
class Meta:
|
|
241
|
+
model = Organization
|
|
242
|
+
fields = ['number_class_code_len', 'number_item_len', 'number_variation_len', ]
|
|
243
|
+
labels = {
|
|
244
|
+
"number_class_code_len": "Number Class Code Length (C)",
|
|
245
|
+
"number_item_len": "Number Item Length (N)",
|
|
246
|
+
"number_variation_len": "Number Variation Length (V)",
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
def __init__(self, *args, **kwargs):
|
|
250
|
+
self.organization = kwargs.get('instance', None)
|
|
251
|
+
super(OrganizationNumberLenForm, self).__init__(*args, **kwargs)
|
|
252
|
+
# self.fields['number_class_code_len'].validators.append(MinValueValidator(self.organization.number_class_code_len))
|
|
253
|
+
# self.fields['number_item_len'].validators.append(MinValueValidator(self.organization.number_item_len))
|
|
254
|
+
# self.fields['number_variation_len'].validators.append(MinValueValidator(self.organization.number_variation_len))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class PartInfoForm(forms.Form):
|
|
258
|
+
quantity = forms.IntegerField(label='Quantity for Est Cost', min_value=1)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class ManufacturerForm(forms.ModelForm):
|
|
262
|
+
class Meta:
|
|
263
|
+
model = Manufacturer
|
|
264
|
+
exclude = ['organization', ]
|
|
265
|
+
|
|
266
|
+
def __init__(self, *args, **kwargs):
|
|
267
|
+
super().__init__(*args, **kwargs)
|
|
268
|
+
self.fields['name'].required = False
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class ManufacturerPartForm(forms.ModelForm):
|
|
272
|
+
class Meta:
|
|
273
|
+
model = ManufacturerPart
|
|
274
|
+
exclude = ['part', ]
|
|
275
|
+
|
|
276
|
+
field_order = ['manufacturer_part_number', 'manufacturer']
|
|
277
|
+
|
|
278
|
+
def __init__(self, *args, **kwargs):
|
|
279
|
+
self.organization = kwargs.pop('organization', None)
|
|
280
|
+
super(ManufacturerPartForm, self).__init__(*args, **kwargs)
|
|
281
|
+
self.fields['manufacturer'].required = False
|
|
282
|
+
self.fields['manufacturer_part_number'].required = False
|
|
283
|
+
self.fields['manufacturer'].queryset = Manufacturer.objects.filter(organization=self.organization).order_by('name')
|
|
284
|
+
self.fields['mouser_disable'].initial = True
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class SellerForm(forms.ModelForm):
|
|
288
|
+
class Meta:
|
|
289
|
+
model = Seller
|
|
290
|
+
exclude = ['organization', ]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class SellerPartForm(forms.ModelForm):
|
|
294
|
+
class Meta:
|
|
295
|
+
model = SellerPart
|
|
296
|
+
exclude = ['manufacturer_part', 'data_source', ]
|
|
297
|
+
|
|
298
|
+
new_seller = forms.CharField(max_length=128, label='-or- Create new seller (leave blank if selecting)', required=False)
|
|
299
|
+
field_order = ['seller', 'new_seller', 'unit_cost', 'nre_cost', 'lead_time_days', 'minimum_order_quantity', 'minimum_pack_quantity', ]
|
|
300
|
+
|
|
301
|
+
def __init__(self, *args, **kwargs):
|
|
302
|
+
self.organization = kwargs.pop('organization', None)
|
|
303
|
+
self.manufacturer_part = kwargs.pop('manufacturer_part', None)
|
|
304
|
+
self.base_fields['unit_cost'] = forms.DecimalField(required=True, decimal_places=4, max_digits=17)
|
|
305
|
+
self.base_fields['nre_cost'] = forms.DecimalField(required=True, decimal_places=4, max_digits=17, label='NRE cost')
|
|
306
|
+
|
|
307
|
+
instance = kwargs.get('instance')
|
|
308
|
+
if instance:
|
|
309
|
+
initial = kwargs.get('initial', {})
|
|
310
|
+
initial['unit_cost'] = instance.unit_cost.amount
|
|
311
|
+
initial['nre_cost'] = instance.nre_cost.amount
|
|
312
|
+
kwargs['initial'] = initial
|
|
313
|
+
super(SellerPartForm, self).__init__(*args, **kwargs)
|
|
314
|
+
if self.manufacturer_part is not None:
|
|
315
|
+
self.instance.manufacturer_part = self.manufacturer_part
|
|
316
|
+
self.fields['seller'].queryset = Seller.objects.filter(organization=self.organization).order_by('name')
|
|
317
|
+
self.fields['seller'].required = False
|
|
318
|
+
|
|
319
|
+
def clean(self):
|
|
320
|
+
cleaned_data = super(SellerPartForm, self).clean()
|
|
321
|
+
seller = cleaned_data.get('seller')
|
|
322
|
+
new_seller = cleaned_data.get('new_seller')
|
|
323
|
+
unit_cost = cleaned_data.get('unit_cost')
|
|
324
|
+
nre_cost = cleaned_data.get('nre_cost')
|
|
325
|
+
if unit_cost is None:
|
|
326
|
+
raise forms.ValidationError("Invalid unit cost.", code='invalid')
|
|
327
|
+
self.instance.unit_cost = Money(unit_cost, self.organization.currency)
|
|
328
|
+
|
|
329
|
+
if nre_cost is None:
|
|
330
|
+
raise forms.ValidationError("Invalid NRE cost.", code='invalid')
|
|
331
|
+
self.instance.nre_cost = Money(nre_cost, self.organization.currency)
|
|
332
|
+
|
|
333
|
+
if seller and new_seller:
|
|
334
|
+
raise forms.ValidationError("Cannot have a seller and a new seller.", code='invalid')
|
|
335
|
+
elif new_seller:
|
|
336
|
+
obj, created = Seller.objects.get_or_create(name__iexact=new_seller, organization=self.organization, defaults={'name': new_seller})
|
|
337
|
+
self.cleaned_data['seller'] = obj
|
|
338
|
+
elif not seller:
|
|
339
|
+
raise forms.ValidationError("Must specify a seller.", code='invalid')
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class PartClassForm(forms.ModelForm):
|
|
343
|
+
class Meta:
|
|
344
|
+
model = PartClass
|
|
345
|
+
fields = ['code', 'name', 'comment']
|
|
346
|
+
|
|
347
|
+
def __init__(self, *args, **kwargs):
|
|
348
|
+
self.organization = kwargs.pop('organization', None)
|
|
349
|
+
self.ignore_unique_constraint = kwargs.pop('ignore_unique_constraint', False)
|
|
350
|
+
super(PartClassForm, self).__init__(*args, **kwargs)
|
|
351
|
+
self.fields['code'].required = False
|
|
352
|
+
self.fields['name'].required = False
|
|
353
|
+
self.fields['code'].validators.extend([MaxLengthValidator(self.organization.number_class_code_len), MinLengthValidator(self.organization.number_class_code_len)])
|
|
354
|
+
|
|
355
|
+
def clean_name(self):
|
|
356
|
+
cleaned_data = super(PartClassForm, self).clean()
|
|
357
|
+
name = cleaned_data.get('name')
|
|
358
|
+
if self.ignore_unique_constraint:
|
|
359
|
+
return name
|
|
360
|
+
try:
|
|
361
|
+
part_class_with_name = PartClass.objects.get(name__iexact=name, organization=self.organization)
|
|
362
|
+
if part_class_with_name and self.instance and self.instance.id != part_class_with_name.id:
|
|
363
|
+
validation_error = forms.ValidationError("Part class with name {} is already defined.".format(name), code='invalid')
|
|
364
|
+
self.add_error('name', validation_error)
|
|
365
|
+
except PartClass.DoesNotExist:
|
|
366
|
+
pass
|
|
367
|
+
return name
|
|
368
|
+
|
|
369
|
+
def clean_code(self):
|
|
370
|
+
cleaned_data = super(PartClassForm, self).clean()
|
|
371
|
+
code = cleaned_data.get('code')
|
|
372
|
+
if self.ignore_unique_constraint:
|
|
373
|
+
return code
|
|
374
|
+
if PartClass.objects.filter(code=code, organization=self.organization).exclude(pk=self.instance.pk).count() > 0:
|
|
375
|
+
validation_error = forms.ValidationError(f"Part class with code {code} is already defined.", code='invalid')
|
|
376
|
+
self.add_error('code', validation_error)
|
|
377
|
+
return code
|
|
378
|
+
|
|
379
|
+
def clean(self):
|
|
380
|
+
cleaned_data = super(PartClassForm, self).clean()
|
|
381
|
+
cleaned_data['organization_id'] = self.organization
|
|
382
|
+
self.instance.organization = self.organization
|
|
383
|
+
return cleaned_data
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
PartClassFormSet = forms.formset_factory(PartClassForm, extra=2, can_delete=True)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class PartClassSelectionForm(forms.Form):
|
|
390
|
+
def __init__(self, *args, **kwargs):
|
|
391
|
+
self.organization = kwargs.pop('organization', None)
|
|
392
|
+
super(PartClassSelectionForm, self).__init__(*args, **kwargs)
|
|
393
|
+
self.fields['part_class'] = forms.CharField(required=False,
|
|
394
|
+
widget=AutocompleteTextInput(attrs={'placeholder': 'Select a part class.'},
|
|
395
|
+
autocomplete_submit=True,
|
|
396
|
+
queryset=PartClass.objects.filter(organization=self.organization)))
|
|
397
|
+
|
|
398
|
+
def clean_part_class(self):
|
|
399
|
+
part_class = self.cleaned_data['part_class']
|
|
400
|
+
if part_class == '':
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
return PartClass.objects.get(organization=self.organization, code=part_class.split(':')[0])
|
|
405
|
+
except PartClass.DoesNotExist:
|
|
406
|
+
pc = PartClass.objects.filter(name__icontains=part_class).order_by('name').first()
|
|
407
|
+
if pc is not None:
|
|
408
|
+
return pc
|
|
409
|
+
else:
|
|
410
|
+
self.add_error('part_class', 'Select a valid part class.')
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class PartClassCSVForm(forms.Form):
|
|
415
|
+
file = forms.FileField(required=False)
|
|
416
|
+
|
|
417
|
+
def __init__(self, *args, **kwargs):
|
|
418
|
+
self.organization = kwargs.pop('organization', None)
|
|
419
|
+
super(PartClassCSVForm, self).__init__(*args, **kwargs)
|
|
420
|
+
|
|
421
|
+
def clean(self):
|
|
422
|
+
cleaned_data = super(PartClassCSVForm, self).clean()
|
|
423
|
+
file = self.cleaned_data.get('file')
|
|
424
|
+
self.successes = list()
|
|
425
|
+
self.warnings = list()
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
csvline_decoded = file.readline().decode('utf-8')
|
|
429
|
+
dialect = csv.Sniffer().sniff(csvline_decoded)
|
|
430
|
+
file.open()
|
|
431
|
+
reader = csv.reader(codecs.iterdecode(file, 'utf-8'), dialect)
|
|
432
|
+
headers = [h.lower().replace('\ufeff', '') for h in next(reader)]
|
|
433
|
+
|
|
434
|
+
csv_headers = PartClassesCSVHeaders()
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
# Issue warning if unrecognized column header names appear in file.
|
|
438
|
+
csv_headers.validate_header_names(headers)
|
|
439
|
+
except CSVHeaderError as e:
|
|
440
|
+
self.warnings.append(e.__str__() + ". Column(s) ignored.")
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
# Make sure that required columns appear in the file, then convert whatever
|
|
444
|
+
# header synonym names were used to default header names.
|
|
445
|
+
hdr_assertions = [
|
|
446
|
+
('comment', 'description', 'mex'), # MUTUALLY EXCLUSIVE part_class or part_number but not both
|
|
447
|
+
('code', 'in'), # CONTAINS revision
|
|
448
|
+
('name', 'in'), # CONTAINS name
|
|
449
|
+
]
|
|
450
|
+
csv_headers.validate_header_assertions(headers, hdr_assertions)
|
|
451
|
+
headers = csv_headers.get_defaults_list(headers)
|
|
452
|
+
except CSVHeaderError as e:
|
|
453
|
+
raise ValidationError(e.__str__() + ". Uploading stopped. No part classes uploaded.", code='invalid')
|
|
454
|
+
|
|
455
|
+
row_count = 1 # Skip over header row
|
|
456
|
+
for row in reader:
|
|
457
|
+
row_count += 1
|
|
458
|
+
part_class_data = {}
|
|
459
|
+
|
|
460
|
+
for idx, hdr in enumerate(headers):
|
|
461
|
+
if idx == len(row): break
|
|
462
|
+
part_class_data[hdr] = row[idx]
|
|
463
|
+
|
|
464
|
+
name = csv_headers.get_val_from_row(part_class_data, 'name')
|
|
465
|
+
code = csv_headers.get_val_from_row(part_class_data, 'code')
|
|
466
|
+
description = csv_headers.get_val_from_row(part_class_data, 'description')
|
|
467
|
+
comment = csv_headers.get_val_from_row(part_class_data, 'comment')
|
|
468
|
+
|
|
469
|
+
try:
|
|
470
|
+
if code is None:
|
|
471
|
+
validation_error = forms.ValidationError(
|
|
472
|
+
"Part class 'code' in row {} does not have a value. Uploading of this part class skipped.".format(row_count),
|
|
473
|
+
code='invalid')
|
|
474
|
+
self.add_error(None, validation_error)
|
|
475
|
+
continue
|
|
476
|
+
elif len(code) != self.organization.number_class_code_len:
|
|
477
|
+
validation_error = forms.ValidationError(
|
|
478
|
+
"Length of part class 'code' in row {} is different than the organization class length {}. Uploading of this part class skipped.".format(row_count, self.organization.number_class_code_len),
|
|
479
|
+
code='invalid')
|
|
480
|
+
self.add_error(None, validation_error)
|
|
481
|
+
continue
|
|
482
|
+
|
|
483
|
+
description_or_comment = ''
|
|
484
|
+
if description is not None:
|
|
485
|
+
description_or_comment = description
|
|
486
|
+
elif comment is not None:
|
|
487
|
+
description_or_comment = comment
|
|
488
|
+
PartClass.objects.create(code=code, name=name, comment=description_or_comment, organization=self.organization)
|
|
489
|
+
self.successes.append("Part class {0} {1} on row {2} created.".format(code, name, row_count))
|
|
490
|
+
|
|
491
|
+
except IntegrityError:
|
|
492
|
+
validation_error = forms.ValidationError(
|
|
493
|
+
"Part class {0} {1} on row {2} is already defined. Uploading of this part class skipped.".format(code, name, row_count),
|
|
494
|
+
code='invalid')
|
|
495
|
+
self.add_error(None, validation_error)
|
|
496
|
+
|
|
497
|
+
except UnicodeDecodeError as e:
|
|
498
|
+
self.add_error(None, forms.ValidationError("CSV File Encoding error, try encoding your file as utf-8, and upload again. \
|
|
499
|
+
If this keeps happening, reach out to info@indabom.com with your csv file and we'll do our best to \
|
|
500
|
+
fix your issue!", code='invalid'))
|
|
501
|
+
logger.warning("UnicodeDecodeError: {}".format(e))
|
|
502
|
+
raise ValidationError("Specific Error: {}".format(e),
|
|
503
|
+
code='invalid')
|
|
504
|
+
|
|
505
|
+
return cleaned_data
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class PartCSVForm(forms.Form):
|
|
509
|
+
file = forms.FileField(required=False)
|
|
510
|
+
|
|
511
|
+
def __init__(self, *args, **kwargs):
|
|
512
|
+
self.organization = kwargs.pop('organization', None)
|
|
513
|
+
super(PartCSVForm, self).__init__(*args, **kwargs)
|
|
514
|
+
|
|
515
|
+
def clean(self):
|
|
516
|
+
cleaned_data = super(PartCSVForm, self).clean()
|
|
517
|
+
file = self.cleaned_data.get('file')
|
|
518
|
+
self.successes = list()
|
|
519
|
+
self.warnings = list()
|
|
520
|
+
|
|
521
|
+
try:
|
|
522
|
+
csvline_decoded = file.readline().decode('utf-8')
|
|
523
|
+
dialect = csv.Sniffer().sniff(csvline_decoded)
|
|
524
|
+
file.open()
|
|
525
|
+
reader = csv.reader(codecs.iterdecode(file, 'utf-8'), dialect)
|
|
526
|
+
headers = [h.lower() for h in next(reader)]
|
|
527
|
+
|
|
528
|
+
# Handle utf-8-sig encoding
|
|
529
|
+
if "\ufeff" in headers[0]:
|
|
530
|
+
reader = csv.reader(codecs.iterdecode(file, 'utf-8-sig'), dialect)
|
|
531
|
+
headers = [h.lower() for h in next(reader)]
|
|
532
|
+
|
|
533
|
+
csv_headers = self.organization.part_list_csv_headers()
|
|
534
|
+
|
|
535
|
+
try:
|
|
536
|
+
# Issue warning if unrecognized column header names appear in file.
|
|
537
|
+
csv_headers.validate_header_names(headers)
|
|
538
|
+
except CSVHeaderError as e:
|
|
539
|
+
self.warnings.append(e.__str__() + ". Columns ignored.")
|
|
540
|
+
|
|
541
|
+
try:
|
|
542
|
+
# Make sure that required columns appear in the file, then convert whatever
|
|
543
|
+
# header synonym names were used to default header names.
|
|
544
|
+
hdr_assertions = [
|
|
545
|
+
('part_class', 'part_number', 'or'), # part_class OR part_number
|
|
546
|
+
('revision', 'in'), # CONTAINS revision
|
|
547
|
+
('value', 'value_units', 'and', 'description', 'or'), # (value AND value units) OR description
|
|
548
|
+
]
|
|
549
|
+
csv_headers.validate_header_assertions(headers, hdr_assertions)
|
|
550
|
+
headers = csv_headers.get_defaults_list(headers)
|
|
551
|
+
except CSVHeaderError as e:
|
|
552
|
+
raise ValidationError(e.__str__() + ". Uploading stopped. No parts uploaded.", code='invalid')
|
|
553
|
+
|
|
554
|
+
row_count = 1 # Skip over header row
|
|
555
|
+
for row in reader:
|
|
556
|
+
row_count += 1
|
|
557
|
+
part_data = {}
|
|
558
|
+
|
|
559
|
+
for idx, hdr in enumerate(headers):
|
|
560
|
+
if idx == len(row): break
|
|
561
|
+
part_data[hdr] = row[idx]
|
|
562
|
+
|
|
563
|
+
part_number = csv_headers.get_val_from_row(part_data, 'part_number')
|
|
564
|
+
part_class = csv_headers.get_val_from_row(part_data, 'part_class')
|
|
565
|
+
number_item = None
|
|
566
|
+
number_variation = None
|
|
567
|
+
revision = csv_headers.get_val_from_row(part_data, 'revision')
|
|
568
|
+
mpn = csv_headers.get_val_from_row(part_data, 'mpn')
|
|
569
|
+
mfg_name = csv_headers.get_val_from_row(part_data, 'mfg_name')
|
|
570
|
+
description = csv_headers.get_val_from_row(part_data, 'description')
|
|
571
|
+
value = csv_headers.get_val_from_row(part_data, 'value')
|
|
572
|
+
value_units = csv_headers.get_val_from_row(part_data, 'value_units')
|
|
573
|
+
seller_name = csv_headers.get_val_from_row(part_data, 'seller')
|
|
574
|
+
seller_part_number = csv_headers.get_val_from_row(part_data, 'seller_part_number')
|
|
575
|
+
unit_cost = csv_headers.get_val_from_row(part_data, 'unit_cost')
|
|
576
|
+
nre_cost = csv_headers.get_val_from_row(part_data, 'part_nre_cost')
|
|
577
|
+
moq = csv_headers.get_val_from_row(part_data, 'moq')
|
|
578
|
+
mpq = csv_headers.get_val_from_row(part_data, 'minimum_pack_quantity')
|
|
579
|
+
|
|
580
|
+
# Check part number for uniqueness. If part number not specified
|
|
581
|
+
# then Part.save() will create one.
|
|
582
|
+
if part_number:
|
|
583
|
+
if self.organization.number_scheme == NUMBER_SCHEME_SEMI_INTELLIGENT:
|
|
584
|
+
try:
|
|
585
|
+
(number_class, number_item, number_variation) = Part.parse_part_number(part_number, self.organization)
|
|
586
|
+
part_class = PartClass.objects.get(code=number_class, organization=self.organization)
|
|
587
|
+
Part.objects.get(number_class=part_class, number_item=number_item, number_variation=number_variation, organization=self.organization)
|
|
588
|
+
self.add_error(None, "Part number {0} in row {1} already exists. Uploading of this part skipped.".format(part_number, row_count))
|
|
589
|
+
continue
|
|
590
|
+
except AttributeError as e:
|
|
591
|
+
self.add_error(None, str(e) + " on row {}. Creation of this part skipped.".format(row_count))
|
|
592
|
+
continue
|
|
593
|
+
except PartClass.DoesNotExist:
|
|
594
|
+
self.add_error(None, "No part class found for part number {0} in row {1}. Creation of this part skipped.".format(part_number, row_count))
|
|
595
|
+
continue
|
|
596
|
+
except Part.DoesNotExist:
|
|
597
|
+
pass
|
|
598
|
+
else:
|
|
599
|
+
try:
|
|
600
|
+
number_item = part_number
|
|
601
|
+
Part.objects.get(number_class=None, number_item=number_item, number_variation=None, organization=self.organization)
|
|
602
|
+
self.add_error(None, f"Part number {part_number} in row {row_count} already exists. Uploading of this part skipped.")
|
|
603
|
+
continue
|
|
604
|
+
except Part.DoesNotExist:
|
|
605
|
+
pass
|
|
606
|
+
elif part_class:
|
|
607
|
+
try:
|
|
608
|
+
part_class = PartClass.objects.get(code=part_data[csv_headers.get_default('part_class')], organization=self.organization)
|
|
609
|
+
except PartClass.DoesNotExist:
|
|
610
|
+
self.add_error(None, "Part class {0} in row {1} doesn't exist. Create part class on Settings > IndaBOM and try again."
|
|
611
|
+
"Uploading of this part skipped.".format(part_data[csv_headers.get_default('part_class')], row_count))
|
|
612
|
+
continue
|
|
613
|
+
else:
|
|
614
|
+
if self.organization.number_scheme == NUMBER_SCHEME_SEMI_INTELLIGENT:
|
|
615
|
+
self.add_error(None, "In row {} need to specify a part_class or part_number. Uploading of this part skipped.".format(row_count))
|
|
616
|
+
else:
|
|
617
|
+
self.add_error(None, "In row {} need to specify a part_number. Uploading of this part skipped.".format(row_count))
|
|
618
|
+
continue
|
|
619
|
+
|
|
620
|
+
if not revision:
|
|
621
|
+
self.add_error(None, f"Missing revision in row {row_count}. Uploading of this part skipped.")
|
|
622
|
+
continue
|
|
623
|
+
elif len(revision) > 4:
|
|
624
|
+
self.add_error(None, "Revision {0} in row {1} is more than the maximum 4 characters. "
|
|
625
|
+
"Uploading of this part skipped.".format(part_data[csv_headers.get_default('revision')], row_count))
|
|
626
|
+
continue
|
|
627
|
+
elif revision.isdigit() and int(revision) < 0:
|
|
628
|
+
self.add_error(None, "Revision {0} in row {1} cannot be a negative number. "
|
|
629
|
+
"Uploading of this part skipped.".format(part_data[csv_headers.get_default('revision')], row_count))
|
|
630
|
+
continue
|
|
631
|
+
|
|
632
|
+
if mpn and mfg_name:
|
|
633
|
+
manufacturer_part = ManufacturerPart.objects.filter(manufacturer_part_number=mpn,
|
|
634
|
+
manufacturer__name=mfg_name,
|
|
635
|
+
manufacturer__organization=self.organization)
|
|
636
|
+
if manufacturer_part.count() > 0:
|
|
637
|
+
self.add_error(None, "Part already exists for manufacturer part {0} in row {1}. "
|
|
638
|
+
"Uploading of this part skipped.".format(row_count, mpn, row_count))
|
|
639
|
+
continue
|
|
640
|
+
|
|
641
|
+
skip = False
|
|
642
|
+
part_revision = PartRevision()
|
|
643
|
+
part_revision.revision = revision
|
|
644
|
+
|
|
645
|
+
# Required properties:
|
|
646
|
+
if description is None:
|
|
647
|
+
if value is None and value_units is None:
|
|
648
|
+
self.add_error(None, "Missing 'description' or 'value' plus 'value_units' for part in row {}. Uploading of this part skipped.".format(row_count))
|
|
649
|
+
skip = True
|
|
650
|
+
break
|
|
651
|
+
elif value is None and value_units is not None:
|
|
652
|
+
self.add_error(None, "Missing 'value' for part in row {}. Uploading of this part skipped.".format(row_count))
|
|
653
|
+
skip = True
|
|
654
|
+
break
|
|
655
|
+
elif value is not None and value_units is None:
|
|
656
|
+
self.add_error(None, "Missing 'value_units' for part in row {}. Uploading of this part skipped.".format(row_count))
|
|
657
|
+
skip = True
|
|
658
|
+
break
|
|
659
|
+
|
|
660
|
+
part_revision.description = description
|
|
661
|
+
|
|
662
|
+
# Part revision's value and value_units set below, after have had a chance to validate unit choice.
|
|
663
|
+
|
|
664
|
+
def is_valid_choice(choice, choices):
|
|
665
|
+
for c in choices:
|
|
666
|
+
if choice == c[0]:
|
|
667
|
+
return True
|
|
668
|
+
return False
|
|
669
|
+
|
|
670
|
+
# Optional properties with free-form values:
|
|
671
|
+
props_free_form = ['tolerance', 'pin_count', 'color', 'material', 'finish', 'attribute']
|
|
672
|
+
for prop_free_form in props_free_form:
|
|
673
|
+
prop_free_form = csv_headers.get_default(prop_free_form)
|
|
674
|
+
if prop_free_form in part_data:
|
|
675
|
+
setattr(part_revision, prop_free_form, part_data[prop_free_form])
|
|
676
|
+
|
|
677
|
+
# Optional properties with choices for values:
|
|
678
|
+
props_with_value_choices = {'package': PACKAGE_TYPES, 'interface': INTERFACE_TYPES}
|
|
679
|
+
for k, v in props_with_value_choices.items():
|
|
680
|
+
k = csv_headers.get_default(k)
|
|
681
|
+
if k in part_data:
|
|
682
|
+
if is_valid_choice(part_data[k], v):
|
|
683
|
+
setattr(part_revision, k, part_data[k])
|
|
684
|
+
else:
|
|
685
|
+
self.warnings.append("'{0}' is an invalid choice of value for '{1}' for part in row {2} . Uploading of this property skipped. "
|
|
686
|
+
"Part will still be uploaded".format(part_data[k], k, row_count))
|
|
687
|
+
|
|
688
|
+
# Optional properties with units:
|
|
689
|
+
props_with_unit_choices = {
|
|
690
|
+
'value': VALUE_UNITS,
|
|
691
|
+
'supply_voltage': VOLTAGE_UNITS, 'power_rating': POWER_UNITS,
|
|
692
|
+
'voltage_rating': VOLTAGE_UNITS, 'current_rating': CURRENT_UNITS,
|
|
693
|
+
'temperature_rating': TEMPERATURE_UNITS, 'memory': MEMORY_UNITS,
|
|
694
|
+
'frequency': FREQUENCY_UNITS, 'wavelength': WAVELENGTH_UNITS,
|
|
695
|
+
'length': DISTANCE_UNITS, 'width': DISTANCE_UNITS,
|
|
696
|
+
'height': DISTANCE_UNITS, 'weight': WEIGHT_UNITS,
|
|
697
|
+
}
|
|
698
|
+
for k, v in props_with_unit_choices.items():
|
|
699
|
+
k = csv_headers.get_default(k)
|
|
700
|
+
if k in part_data and k + '_units' in part_data:
|
|
701
|
+
if part_data[k] and not part_data[k + '_units']:
|
|
702
|
+
self.add_error(None, "Missing '{0}' for part in row {1}. Uploading of this part skipped.".format(k, row_count))
|
|
703
|
+
skip = True
|
|
704
|
+
break
|
|
705
|
+
elif not part_data[k] and part_data[k + '_units']:
|
|
706
|
+
self.add_error(None, "Missing '{0}' for part in row {1}. Uploading of this part skipped.".format(k + '_units', row_count))
|
|
707
|
+
skip = True
|
|
708
|
+
break
|
|
709
|
+
elif part_data[k + '_units']:
|
|
710
|
+
if is_valid_choice(part_data[k + '_units'], v):
|
|
711
|
+
setattr(part_revision, k, part_data[k])
|
|
712
|
+
setattr(part_revision, k + '_units', part_data[k + '_units'])
|
|
713
|
+
else:
|
|
714
|
+
self.add_error(None, "'{0}' is an invalid choice of units for '{1}' for part in row {2}. Uploading of this part skipped."
|
|
715
|
+
.format(part_data[k + '_units'], k + '_units', row_count))
|
|
716
|
+
skip = True
|
|
717
|
+
break
|
|
718
|
+
|
|
719
|
+
if self.organization.number_scheme == NUMBER_SCHEME_INTELLIGENT and number_item is None:
|
|
720
|
+
self.add_error(None, "Can't upload a part without a number_item header for part in row {}. Uploading of this part skipped.".format(row_count))
|
|
721
|
+
skip = True
|
|
722
|
+
|
|
723
|
+
if skip:
|
|
724
|
+
continue
|
|
725
|
+
|
|
726
|
+
PartForm = part_form_from_organization(self.organization)
|
|
727
|
+
part = Part(number_class=part_class, number_item=number_item, number_variation=number_variation, organization=self.organization)
|
|
728
|
+
part_dict = model_to_dict(part)
|
|
729
|
+
part_dict.update({'number_class': str(part.number_class)})
|
|
730
|
+
pf = PartForm(data=part_dict, organization=self.organization)
|
|
731
|
+
prf = PartRevisionForm(data=model_to_dict(part_revision))
|
|
732
|
+
|
|
733
|
+
if pf.is_valid() and prf.is_valid():
|
|
734
|
+
part = pf.save(commit=False)
|
|
735
|
+
part.organization = self.organization
|
|
736
|
+
part.save()
|
|
737
|
+
part_revision = prf.save(commit=False)
|
|
738
|
+
part_revision.part = part
|
|
739
|
+
part_revision.save()
|
|
740
|
+
|
|
741
|
+
if mfg_name and mpn:
|
|
742
|
+
mfg, created = Manufacturer.objects.get_or_create(name__iexact=mfg_name, organization=self.organization, defaults={'name': mfg_name})
|
|
743
|
+
manufacturer_part, created = ManufacturerPart.objects.get_or_create(part=part, manufacturer_part_number=mpn, manufacturer=mfg)
|
|
744
|
+
if part.primary_manufacturer_part is None and manufacturer_part is not None:
|
|
745
|
+
part.primary_manufacturer_part = manufacturer_part
|
|
746
|
+
part.save()
|
|
747
|
+
|
|
748
|
+
if seller_name and unit_cost and nre_cost:
|
|
749
|
+
seller, created = Seller.objects.get_or_create(name__iexact=seller_name, organization=self.organization, defaults={'name': seller_name})
|
|
750
|
+
seller_part, created = SellerPart.objects.get_or_create(manufacturer_part=manufacturer_part, seller=seller, seller_part_number=seller_part_number, unit_cost=unit_cost, nre_cost=nre_cost, minimum_order_quantity=moq, minimum_pack_quantity=mpq)
|
|
751
|
+
|
|
752
|
+
self.successes.append("Part {0} on row {1} created.".format(part.full_part_number(), row_count))
|
|
753
|
+
else:
|
|
754
|
+
for k, error in prf.errors.items():
|
|
755
|
+
for idx, msg in enumerate(error):
|
|
756
|
+
error[idx] = f"Error on Row {row_count}, {k}: " + msg
|
|
757
|
+
self.errors.update({k: error})
|
|
758
|
+
for k, error in pf.errors.items():
|
|
759
|
+
for idx, msg in enumerate(error):
|
|
760
|
+
error[idx] = f"Error on Row {row_count}, {k}: " + msg
|
|
761
|
+
self.errors.update({k: error})
|
|
762
|
+
|
|
763
|
+
# part = Part.objects.create(number_class=part_class, number_item=number_item, number_variation=number_variation, organization=self.organization)
|
|
764
|
+
|
|
765
|
+
except UnicodeDecodeError as e:
|
|
766
|
+
self.add_error(None, forms.ValidationError("CSV File Encoding error, try encoding your file as utf-8, and upload again. \
|
|
767
|
+
If this keeps happening, reach out to info@indabom.com with your csv file and we'll do our best to \
|
|
768
|
+
fix your issue!", code='invalid'))
|
|
769
|
+
logger.warning("UnicodeDecodeError: {}".format(e))
|
|
770
|
+
raise ValidationError("Specific Error: {}".format(e), code='invalid')
|
|
771
|
+
|
|
772
|
+
return cleaned_data
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
class PartFormIntelligent(forms.ModelForm):
|
|
776
|
+
class Meta:
|
|
777
|
+
model = Part
|
|
778
|
+
exclude = ['number_class', 'number_variation', 'organization', 'google_drive_parent', ]
|
|
779
|
+
help_texts = {
|
|
780
|
+
'number_item': _('Enter a part number.'),
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
def __init__(self, *args, **kwargs):
|
|
784
|
+
self.organization = kwargs.pop('organization', None)
|
|
785
|
+
self.ignore_number_class = kwargs.pop('ignore_part_class', False)
|
|
786
|
+
self.ignore_unique_constraint = kwargs.pop('ignore_unique_constraint', False)
|
|
787
|
+
super(PartFormIntelligent, self).__init__(*args, **kwargs)
|
|
788
|
+
self.fields['number_item'].required = True
|
|
789
|
+
if self.instance and self.instance.id:
|
|
790
|
+
self.fields['primary_manufacturer_part'].queryset = ManufacturerPart.objects.filter(part__id=self.instance.id).order_by('manufacturer_part_number')
|
|
791
|
+
else:
|
|
792
|
+
del self.fields['primary_manufacturer_part']
|
|
793
|
+
for _, value in self.fields.items():
|
|
794
|
+
value.widget.attrs['placeholder'] = value.help_text
|
|
795
|
+
value.help_text = ''
|
|
796
|
+
|
|
797
|
+
class PartFormSemiIntelligent(forms.ModelForm):
|
|
798
|
+
class Meta:
|
|
799
|
+
model = Part
|
|
800
|
+
exclude = ['organization', 'google_drive_parent', ]
|
|
801
|
+
help_texts = {
|
|
802
|
+
'number_item': _('Auto generated if blank.'),
|
|
803
|
+
'number_variation': 'Auto generated if blank.',
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
def __init__(self, *args, **kwargs):
|
|
807
|
+
self.organization = kwargs.pop('organization', None)
|
|
808
|
+
self.ignore_number_class = kwargs.pop('ignore_part_class', False)
|
|
809
|
+
self.ignore_unique_constraint = kwargs.pop('ignore_unique_constraint', False)
|
|
810
|
+
super(PartFormSemiIntelligent, self).__init__(*args, **kwargs)
|
|
811
|
+
self.fields['number_item'].validators.append(alphanumeric)
|
|
812
|
+
self.fields['number_class'] = forms.CharField(label='Part Number Class*', required=True, help_text='Select a number class.',
|
|
813
|
+
widget=AutocompleteTextInput(queryset=PartClass.objects.filter(organization=self.organization)))
|
|
814
|
+
if kwargs.get('instance', None): # To check uniqueness
|
|
815
|
+
self.id = kwargs['instance'].id
|
|
816
|
+
|
|
817
|
+
if self.instance and self.instance.id:
|
|
818
|
+
self.fields['primary_manufacturer_part'].queryset = ManufacturerPart.objects.filter(part__id=self.instance.id).order_by('manufacturer_part_number')
|
|
819
|
+
else:
|
|
820
|
+
del self.fields['primary_manufacturer_part']
|
|
821
|
+
for _, value in self.fields.items():
|
|
822
|
+
value.widget.attrs['placeholder'] = value.help_text
|
|
823
|
+
value.help_text = ''
|
|
824
|
+
|
|
825
|
+
if self.initial.get('number_class'):
|
|
826
|
+
try:
|
|
827
|
+
part_class = PartClass.objects.get(id=self.initial['number_class'])
|
|
828
|
+
self.initial['number_class'] = str(part_class)
|
|
829
|
+
except PartClass.DoesNotExist:
|
|
830
|
+
self.initial['number_class'] = ""
|
|
831
|
+
|
|
832
|
+
if self.ignore_number_class:
|
|
833
|
+
self.fields.get('number_class').required = False
|
|
834
|
+
|
|
835
|
+
def clean_number_class(self):
|
|
836
|
+
if self.ignore_number_class:
|
|
837
|
+
return None
|
|
838
|
+
number_class = self.cleaned_data['number_class']
|
|
839
|
+
try:
|
|
840
|
+
return PartClass.objects.get(organization=self.organization, code=number_class.split(':')[0])
|
|
841
|
+
except PartClass.DoesNotExist:
|
|
842
|
+
self.add_error('number_class', f'Select an existing part class, or create `{number_class}` in Settings.')
|
|
843
|
+
return None
|
|
844
|
+
|
|
845
|
+
def clean(self):
|
|
846
|
+
cleaned_data = super(PartFormSemiIntelligent, self).clean()
|
|
847
|
+
number_item = cleaned_data.get('number_item')
|
|
848
|
+
number_class = cleaned_data.get('number_class')
|
|
849
|
+
number_variation = cleaned_data.get('number_variation')
|
|
850
|
+
|
|
851
|
+
try:
|
|
852
|
+
if number_class is not None and number_class.code != '':
|
|
853
|
+
Part.verify_format_number_class(number_class.code, self.organization)
|
|
854
|
+
except AttributeError as e:
|
|
855
|
+
validation_error = forms.ValidationError(str(e), code='invalid')
|
|
856
|
+
self.add_error('number_class', validation_error)
|
|
857
|
+
|
|
858
|
+
try:
|
|
859
|
+
if number_item is not None and number_item != '':
|
|
860
|
+
Part.verify_format_number_item(number_item, self.organization)
|
|
861
|
+
except AttributeError as e:
|
|
862
|
+
validation_error = forms.ValidationError(str(e), code='invalid')
|
|
863
|
+
self.add_error('number_item', validation_error)
|
|
864
|
+
|
|
865
|
+
try:
|
|
866
|
+
if number_variation:
|
|
867
|
+
Part.verify_format_number_variation(number_variation, self.organization)
|
|
868
|
+
except AttributeError as e:
|
|
869
|
+
validation_error = forms.ValidationError(str(e), code='invalid')
|
|
870
|
+
self.add_error('number_variation', validation_error)
|
|
871
|
+
|
|
872
|
+
if self.ignore_unique_constraint:
|
|
873
|
+
return cleaned_data
|
|
874
|
+
|
|
875
|
+
part = Part.objects.filter(
|
|
876
|
+
number_class=number_class,
|
|
877
|
+
number_item=number_item,
|
|
878
|
+
number_variation=number_variation,
|
|
879
|
+
organization=self.organization
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
try:
|
|
883
|
+
part = part.exclude(pk=self.id)
|
|
884
|
+
except AttributeError:
|
|
885
|
+
pass
|
|
886
|
+
|
|
887
|
+
if part.count() > 0:
|
|
888
|
+
validation_error = forms.ValidationError(
|
|
889
|
+
("Part number {0}-{1}-{2} already in use.".format(number_class.code, number_item, number_variation)),
|
|
890
|
+
code='invalid')
|
|
891
|
+
self.add_error(None, validation_error)
|
|
892
|
+
return cleaned_data
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
class PartRevisionForm(forms.ModelForm):
|
|
896
|
+
class Meta:
|
|
897
|
+
model = PartRevision
|
|
898
|
+
exclude = ['timestamp', 'assembly', 'part']
|
|
899
|
+
help_texts = {
|
|
900
|
+
'description': _('Additional part info, special instructions, etc.'),
|
|
901
|
+
'attribute': _('Additional part attributes (free form)'),
|
|
902
|
+
'value': _('Number or text'),
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
def __init__(self, *args, **kwargs):
|
|
906
|
+
super(PartRevisionForm, self).__init__(*args, **kwargs)
|
|
907
|
+
|
|
908
|
+
self.fields['revision'].initial = 1
|
|
909
|
+
self.fields['configuration'].required = False
|
|
910
|
+
|
|
911
|
+
self.fields['tolerance'].initial = '%'
|
|
912
|
+
|
|
913
|
+
# Fix up field labels to be succinct for use in rendered form:
|
|
914
|
+
for f in self.fields.values():
|
|
915
|
+
if 'units' in f.label: f.label = 'Units'
|
|
916
|
+
f.label.replace('rating', '')
|
|
917
|
+
# f.value = strip_trailing_zeros(f.value) # Harmless if field is not a number
|
|
918
|
+
self.fields['supply_voltage'].label = 'Vsupply'
|
|
919
|
+
self.fields['attribute'].label = ''
|
|
920
|
+
|
|
921
|
+
for _, value in self.fields.items():
|
|
922
|
+
value.widget.attrs['placeholder'] = value.help_text
|
|
923
|
+
value.help_text = ''
|
|
924
|
+
|
|
925
|
+
if self.instance and not self.data.get('description') and self.instance.description:
|
|
926
|
+
self.data['description'] = self.instance.description
|
|
927
|
+
|
|
928
|
+
def clean(self):
|
|
929
|
+
cleaned_data = super(PartRevisionForm, self).clean()
|
|
930
|
+
|
|
931
|
+
if not cleaned_data.get('description') and not cleaned_data.get('value'):
|
|
932
|
+
validation_error = forms.ValidationError("Must specify either a description or both value and value units.", code='invalid')
|
|
933
|
+
self.add_error('description', validation_error)
|
|
934
|
+
self.add_error('value', validation_error)
|
|
935
|
+
|
|
936
|
+
for key in self.fields.keys():
|
|
937
|
+
if '_units' in key:
|
|
938
|
+
value_name = str.replace(key, '_units', '')
|
|
939
|
+
value = cleaned_data.get(value_name)
|
|
940
|
+
units = cleaned_data.get(key)
|
|
941
|
+
if not value and units:
|
|
942
|
+
self.add_error(key, forms.ValidationError(f"Cannot specify {units} without an accompanying value", code='invalid'))
|
|
943
|
+
elif units and not units:
|
|
944
|
+
self.add_error(key, forms.ValidationError(f"Cannot specify value {units} without an accompanying units", code='invalid'))
|
|
945
|
+
|
|
946
|
+
return cleaned_data
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
class PartRevisionNewForm(PartRevisionForm):
|
|
950
|
+
copy_assembly = forms.BooleanField(label='Copy assembly from latest revision', initial=True, required=False)
|
|
951
|
+
|
|
952
|
+
def __init__(self, *args, **kwargs):
|
|
953
|
+
self.part = kwargs.pop('part', None)
|
|
954
|
+
self.revision = kwargs.pop('revision', None)
|
|
955
|
+
self.assembly = kwargs.pop('assembly', None)
|
|
956
|
+
super(PartRevisionNewForm, self).__init__(*args, **kwargs)
|
|
957
|
+
for _, value in self.fields.items():
|
|
958
|
+
value.widget.attrs['placeholder'] = value.help_text
|
|
959
|
+
value.help_text = ''
|
|
960
|
+
|
|
961
|
+
def save(self):
|
|
962
|
+
cleaned_data = super(PartRevisionNewForm, self).clean()
|
|
963
|
+
self.instance.part = self.part
|
|
964
|
+
self.instance.revision = self.revision
|
|
965
|
+
self.instance.assembly = self.assembly
|
|
966
|
+
self.instance.save()
|
|
967
|
+
return self.instance
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
class SubpartForm(forms.ModelForm):
|
|
971
|
+
class Meta:
|
|
972
|
+
model = Subpart
|
|
973
|
+
fields = ['part_revision', 'reference', 'count', 'do_not_load']
|
|
974
|
+
|
|
975
|
+
def __init__(self, *args, **kwargs):
|
|
976
|
+
self.organization = kwargs.pop('organization', None)
|
|
977
|
+
self.part_id = kwargs.pop('part_id', None)
|
|
978
|
+
self.ignore_part_revision = kwargs.pop('ignore_part_revision', False)
|
|
979
|
+
super(SubpartForm, self).__init__(*args, **kwargs)
|
|
980
|
+
if self.part_id is None:
|
|
981
|
+
self.Meta.exclude = ['part_revision']
|
|
982
|
+
else:
|
|
983
|
+
self.fields['part_revision'].queryset = PartRevision.objects.filter(
|
|
984
|
+
part__id=self.part_id).order_by('-timestamp')
|
|
985
|
+
if self.ignore_part_revision:
|
|
986
|
+
self.fields.get('part_revision').required = False
|
|
987
|
+
|
|
988
|
+
if self.part_id:
|
|
989
|
+
part = Part.objects.get(id=self.part_id)
|
|
990
|
+
unusable_part_ids = [p.id for p in part.where_used_full()]
|
|
991
|
+
unusable_part_ids.append(part.id)
|
|
992
|
+
|
|
993
|
+
def clean_count(self):
|
|
994
|
+
count = self.cleaned_data['count']
|
|
995
|
+
if not count:
|
|
996
|
+
count = 0
|
|
997
|
+
return count
|
|
998
|
+
|
|
999
|
+
def clean_reference(self):
|
|
1000
|
+
reference = self.cleaned_data['reference']
|
|
1001
|
+
reference = stringify_list(listify_string(reference))
|
|
1002
|
+
return reference
|
|
1003
|
+
|
|
1004
|
+
def clean_part_revision(self):
|
|
1005
|
+
if self.ignore_part_revision:
|
|
1006
|
+
return None
|
|
1007
|
+
part_revision = self.cleaned_data['part_revision']
|
|
1008
|
+
return part_revision
|
|
1009
|
+
|
|
1010
|
+
def clean(self):
|
|
1011
|
+
cleaned_data = super(SubpartForm, self).clean()
|
|
1012
|
+
reference_list = listify_string(cleaned_data.get('reference'))
|
|
1013
|
+
count = cleaned_data.get('count')
|
|
1014
|
+
|
|
1015
|
+
if len(reference_list) > 0 and len(reference_list) != count:
|
|
1016
|
+
raise forms.ValidationError(
|
|
1017
|
+
("The number of reference designators ({0}) did not match the subpart quantity ({1}).".format(len(reference_list), count)),
|
|
1018
|
+
code='invalid')
|
|
1019
|
+
|
|
1020
|
+
return cleaned_data
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
class AddSubpartForm(forms.Form):
|
|
1024
|
+
subpart_part_number = forms.CharField(required=True, label="Subpart part number")
|
|
1025
|
+
count = forms.FloatField(required=False, label='Quantity')
|
|
1026
|
+
reference = forms.CharField(required=False, label="Reference")
|
|
1027
|
+
do_not_load = forms.BooleanField(required=False, label="do_not_load")
|
|
1028
|
+
|
|
1029
|
+
def __init__(self, *args, **kwargs):
|
|
1030
|
+
self.organization = kwargs.pop('organization', None)
|
|
1031
|
+
self.part_id = kwargs.pop('part_id', None)
|
|
1032
|
+
self.part = Part.objects.get(id=self.part_id)
|
|
1033
|
+
self.part_revision = self.part.latest()
|
|
1034
|
+
self.unusable_part_rev_ids = [pr.id for pr in self.part_revision.where_used_full()]
|
|
1035
|
+
self.unusable_part_rev_ids.append(self.part_revision.id)
|
|
1036
|
+
super(AddSubpartForm, self).__init__(*args, **kwargs)
|
|
1037
|
+
self.fields['subpart_part_number'] = forms.CharField(required=True, label="Subpart part number",
|
|
1038
|
+
widget=AutocompleteTextInput(attrs={'placeholder': 'Select a part.'},
|
|
1039
|
+
queryset=Part.objects.filter(organization=self.organization).exclude(id=self.part_id),
|
|
1040
|
+
verbose_string_function=Part.verbose_str))
|
|
1041
|
+
|
|
1042
|
+
def clean_count(self):
|
|
1043
|
+
count = self.cleaned_data['count']
|
|
1044
|
+
if not count:
|
|
1045
|
+
count = 0
|
|
1046
|
+
return count
|
|
1047
|
+
|
|
1048
|
+
def clean_reference(self):
|
|
1049
|
+
reference = self.cleaned_data['reference']
|
|
1050
|
+
reference = stringify_list(listify_string(reference))
|
|
1051
|
+
return reference
|
|
1052
|
+
|
|
1053
|
+
def clean_subpart_part_number(self):
|
|
1054
|
+
subpart_part_number = self.cleaned_data['subpart_part_number']
|
|
1055
|
+
|
|
1056
|
+
if not subpart_part_number:
|
|
1057
|
+
validation_error = forms.ValidationError("Must specify a part number.", code='invalid')
|
|
1058
|
+
self.add_error('subpart_part_number', validation_error)
|
|
1059
|
+
|
|
1060
|
+
try:
|
|
1061
|
+
if self.organization.number_scheme == NUMBER_SCHEME_INTELLIGENT:
|
|
1062
|
+
part = Part.objects.get(number_item=subpart_part_number, organization=self.organization)
|
|
1063
|
+
else:
|
|
1064
|
+
(number_class, number_item, number_variation) = Part.parse_partial_part_number(subpart_part_number, self.organization, validate=False)
|
|
1065
|
+
part_class = PartClass.objects.get(code=number_class, organization=self.organization)
|
|
1066
|
+
part = Part.objects.get(number_class=part_class, number_item=number_item, number_variation=number_variation, organization=self.organization)
|
|
1067
|
+
self.subpart_part = part.latest()
|
|
1068
|
+
if self.subpart_part is None:
|
|
1069
|
+
self.add_error('subpart_part_number', f"No part revision exists for part {part.full_part_number()}. Create a revision before adding to an assembly.")
|
|
1070
|
+
return subpart_part_number
|
|
1071
|
+
if self.subpart_part.id in self.unusable_part_rev_ids:
|
|
1072
|
+
validation_error = forms.ValidationError("Infinite recursion! Can't add a part to its self.", code='invalid')
|
|
1073
|
+
self.add_error('subpart_part_number', validation_error)
|
|
1074
|
+
except AttributeError as e:
|
|
1075
|
+
validation_error = forms.ValidationError("Ill-formed subpart part number... " + str(e) + ".", code='invalid')
|
|
1076
|
+
self.add_error('subpart_part_number', validation_error)
|
|
1077
|
+
except PartClass.DoesNotExist:
|
|
1078
|
+
validation_error = forms.ValidationError(f"No part class for in given part number {subpart_part_number}.", code='invalid')
|
|
1079
|
+
self.add_error('subpart_part_number', validation_error)
|
|
1080
|
+
except Part.DoesNotExist:
|
|
1081
|
+
validation_error = forms.ValidationError(f"No part found with given part number {subpart_part_number}.", code='invalid')
|
|
1082
|
+
self.add_error('subpart_part_number', validation_error)
|
|
1083
|
+
|
|
1084
|
+
return subpart_part_number
|
|
1085
|
+
|
|
1086
|
+
def clean(self):
|
|
1087
|
+
cleaned_data = super(AddSubpartForm, self).clean()
|
|
1088
|
+
reference_list = listify_string(cleaned_data.get('reference'))
|
|
1089
|
+
count = cleaned_data.get('count')
|
|
1090
|
+
|
|
1091
|
+
if len(reference_list) > 0 and len(reference_list) != count:
|
|
1092
|
+
raise forms.ValidationError(
|
|
1093
|
+
("The number of reference designators ({0}) did not match the subpart quantity ({1}).".format(len(reference_list), count)),
|
|
1094
|
+
code='invalid')
|
|
1095
|
+
|
|
1096
|
+
return cleaned_data
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
class UploadBOMForm(forms.Form):
|
|
1100
|
+
parent_part_number = forms.CharField(required=False, label="Parent part number")
|
|
1101
|
+
|
|
1102
|
+
def __init__(self, *args, **kwargs):
|
|
1103
|
+
self.organization = kwargs.pop('organization', None)
|
|
1104
|
+
self.parent_part = kwargs.pop('parent_part', None)
|
|
1105
|
+
super(UploadBOMForm, self).__init__(*args, **kwargs)
|
|
1106
|
+
|
|
1107
|
+
def clean_parent_part_number(self):
|
|
1108
|
+
parent_part_number = self.cleaned_data['parent_part_number']
|
|
1109
|
+
|
|
1110
|
+
if parent_part_number: # not required, so only validate if its provided
|
|
1111
|
+
try:
|
|
1112
|
+
self.parent_part = Part.from_part_number(parent_part_number, self.organization)
|
|
1113
|
+
except AttributeError as e:
|
|
1114
|
+
validation_error = forms.ValidationError("Ill-formed parent part number... " + str(e) + ".", code='invalid')
|
|
1115
|
+
self.add_error('parent_part_number', validation_error)
|
|
1116
|
+
except Part.DoesNotExist:
|
|
1117
|
+
validation_error = forms.ValidationError(("Parent part not found with given part number {}.".format(parent_part_number)), code='invalid')
|
|
1118
|
+
self.add_error('parent_part_number', validation_error)
|
|
1119
|
+
|
|
1120
|
+
return parent_part_number
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
class BOMCSVForm(forms.Form):
|
|
1124
|
+
file = forms.FileField(required=False)
|
|
1125
|
+
|
|
1126
|
+
def __init__(self, *args, **kwargs):
|
|
1127
|
+
self.organization = kwargs.pop('organization', None)
|
|
1128
|
+
self.parent_part = kwargs.pop('parent_part', None)
|
|
1129
|
+
super(BOMCSVForm, self).__init__(*args, **kwargs)
|
|
1130
|
+
|
|
1131
|
+
def clean(self):
|
|
1132
|
+
cleaned_data = super(BOMCSVForm, self).clean()
|
|
1133
|
+
file = self.cleaned_data.get('file')
|
|
1134
|
+
self.successes = list()
|
|
1135
|
+
self.warnings = list()
|
|
1136
|
+
|
|
1137
|
+
try:
|
|
1138
|
+
csv_row_decoded = file.readline().decode('utf-8')
|
|
1139
|
+
dialect = csv.Sniffer().sniff(csv_row_decoded)
|
|
1140
|
+
file.open()
|
|
1141
|
+
|
|
1142
|
+
reader = csv.reader(codecs.iterdecode(file, 'utf-8'), dialect, quotechar='"', escapechar='\\')
|
|
1143
|
+
headers = [h.lower() for h in next(reader)]
|
|
1144
|
+
|
|
1145
|
+
# Handle utf-8-sig encoding
|
|
1146
|
+
if len(headers) > 0 and "\ufeff" in headers[0]:
|
|
1147
|
+
reader = csv.reader(codecs.iterdecode(file, 'utf-8-sig'), dialect, quotechar='"', escapechar='\\')
|
|
1148
|
+
headers = [h.lower() for h in next(reader)]
|
|
1149
|
+
elif len(headers) == 0:
|
|
1150
|
+
self.warnings.append("No headers found in CSV file.")
|
|
1151
|
+
|
|
1152
|
+
csv_headers = BOMIndentedCSVHeaders()
|
|
1153
|
+
|
|
1154
|
+
try:
|
|
1155
|
+
# Issue warning if unrecognized column header names appear in file.
|
|
1156
|
+
csv_headers.validate_header_names(headers)
|
|
1157
|
+
except CSVHeaderError as e:
|
|
1158
|
+
self.warnings.append(e.__str__() + ". Columns ignored.")
|
|
1159
|
+
|
|
1160
|
+
try:
|
|
1161
|
+
# Make sure that required columns appear in the file, then convert whatever
|
|
1162
|
+
# header synonym names were used to default header names.
|
|
1163
|
+
hdr_assertions = [
|
|
1164
|
+
('part_number', 'manufacturer_part_number', 'or'), # part_class OR part_number
|
|
1165
|
+
('quantity', 'in'), # CONTAINS quantity
|
|
1166
|
+
]
|
|
1167
|
+
csv_headers.validate_header_assertions(headers, hdr_assertions)
|
|
1168
|
+
headers = csv_headers.get_defaults_list(headers)
|
|
1169
|
+
except CSVHeaderError as e:
|
|
1170
|
+
raise ValidationError(e.__str__() + ". Uploading stopped. No subparts uploaded.", code='invalid')
|
|
1171
|
+
|
|
1172
|
+
parent_part_revision = self.parent_part.latest() if self.parent_part else None
|
|
1173
|
+
|
|
1174
|
+
last_level = None
|
|
1175
|
+
last_part_revision = parent_part_revision
|
|
1176
|
+
part_revision_tree = [] if parent_part_revision is None else [parent_part_revision]
|
|
1177
|
+
|
|
1178
|
+
row_count = 1 # Skip over header row
|
|
1179
|
+
for row in reader:
|
|
1180
|
+
row_count += 1
|
|
1181
|
+
part_dict = {}
|
|
1182
|
+
|
|
1183
|
+
# First prepare data
|
|
1184
|
+
for idx, hdr in enumerate(headers):
|
|
1185
|
+
if idx == len(row): break
|
|
1186
|
+
part_dict[hdr] = row[idx]
|
|
1187
|
+
|
|
1188
|
+
dnp = csv_headers.get_val_from_row(part_dict, 'dnp')
|
|
1189
|
+
reference = csv_headers.get_val_from_row(part_dict, 'reference')
|
|
1190
|
+
part_number = csv_headers.get_val_from_row(part_dict, 'part_number')
|
|
1191
|
+
manufacturer_part_number = csv_headers.get_val_from_row(part_dict, 'mpn')
|
|
1192
|
+
manufacturer_name = csv_headers.get_val_from_row(part_dict, 'manufacturer_name')
|
|
1193
|
+
try:
|
|
1194
|
+
level = int(float(csv_headers.get_val_from_row(part_dict, 'level')))
|
|
1195
|
+
except ValueError as e:
|
|
1196
|
+
# TODO: May want to validate whole file has acceptable levels first.
|
|
1197
|
+
raise ValidationError(f"Row {row_count} - level: invalid level, can't continue.", code='invalid')
|
|
1198
|
+
except TypeError as e:
|
|
1199
|
+
# no level field was provided, we MUST have a parent part number to upload this way, and in this case all levels are the same
|
|
1200
|
+
if parent_part_revision is None:
|
|
1201
|
+
raise ValidationError(f"Row {row_count} - level: must provide either level, or a parent part to upload a part.", code='invalid')
|
|
1202
|
+
else:
|
|
1203
|
+
level = 1
|
|
1204
|
+
|
|
1205
|
+
if last_level is None:
|
|
1206
|
+
last_level = level
|
|
1207
|
+
|
|
1208
|
+
# Extract some values
|
|
1209
|
+
part_dict['reference'] = reference
|
|
1210
|
+
part_dict['do_not_load'] = dnp in ['y', 'x', 'dnp', 'dnl', 'yes', 'true', ]
|
|
1211
|
+
part_dict['revision'] = csv_headers.get_val_from_row(part_dict, 'revision') or 1
|
|
1212
|
+
part_dict['count'] = csv_headers.get_val_from_row(part_dict, 'count')
|
|
1213
|
+
part_dict['number_class'] = None
|
|
1214
|
+
part_dict['number_variation'] = None
|
|
1215
|
+
|
|
1216
|
+
if part_number:
|
|
1217
|
+
try:
|
|
1218
|
+
(part_dict['number_class'], part_dict['number_item'], part_dict['number_variation']) = Part.parse_partial_part_number(part_number, self.organization)
|
|
1219
|
+
except AttributeError as e:
|
|
1220
|
+
self.add_error(None, f"Row {row_count} - part_number: Uploading of this subpart skipped. Couldn't parse part number.")
|
|
1221
|
+
continue
|
|
1222
|
+
elif manufacturer_part_number:
|
|
1223
|
+
try:
|
|
1224
|
+
part = Part.from_manufacturer_part_number(manufacturer_part_number, self.organization)
|
|
1225
|
+
if part is None:
|
|
1226
|
+
self.add_error(None, f"Row {row_count} - manufacturer_part_number: Uploading of this subpart skipped. No part found for manufacturer part number.")
|
|
1227
|
+
continue
|
|
1228
|
+
part_dict['number_class'] = part.number_class.code
|
|
1229
|
+
part_dict['number_item'] = part.number_item
|
|
1230
|
+
part_dict['number_variation'] = part.number_variation
|
|
1231
|
+
part_number = part.full_part_number()
|
|
1232
|
+
except ValueError:
|
|
1233
|
+
self.add_error(None, f"Row {row_count} - manufacturer_part_number: Uploading of this subpart skipped. Too many parts found for manufacturer part number.")
|
|
1234
|
+
continue
|
|
1235
|
+
else:
|
|
1236
|
+
raise ValidationError("No part_number or manufacturer_part_number found. Uploading stopped. No subparts uploaded.", code='invalid')
|
|
1237
|
+
|
|
1238
|
+
# Handle indented bom level changes
|
|
1239
|
+
level_change = level - last_level
|
|
1240
|
+
if level_change == 1: # Level decreases, must only decrease by 1
|
|
1241
|
+
part_revision_tree.append(last_part_revision)
|
|
1242
|
+
elif level_change <= -1: # Level increases, going up in assembly; intentionally empty tree if level change is very negative
|
|
1243
|
+
part_revision_tree = part_revision_tree[:level_change]
|
|
1244
|
+
elif level_change == 0:
|
|
1245
|
+
pass
|
|
1246
|
+
elif level - last_level > 1:
|
|
1247
|
+
raise ValidationError(f'Row {row_count} - level: Assembly levels must decrease by no more than 1 from sequential rows.', code='invalid')
|
|
1248
|
+
else:
|
|
1249
|
+
raise ValidationError(f'Row {row_count} - level: Invalid assembly level.', code='invalid')
|
|
1250
|
+
|
|
1251
|
+
try:
|
|
1252
|
+
parent_part_revision = part_revision_tree[-1]
|
|
1253
|
+
if parent_part_revision.assembly is None:
|
|
1254
|
+
parent_part_revision.assembly = Assembly.objects.create()
|
|
1255
|
+
parent_part_revision.save()
|
|
1256
|
+
except IndexError:
|
|
1257
|
+
parent_part_revision = None
|
|
1258
|
+
|
|
1259
|
+
# Check for existing objects
|
|
1260
|
+
existing_part_class = PartClass.objects.filter(code=part_dict['number_class'], organization=self.organization).first()
|
|
1261
|
+
|
|
1262
|
+
existing_part = None
|
|
1263
|
+
if existing_part_class or self.organization.number_scheme == NUMBER_SCHEME_INTELLIGENT:
|
|
1264
|
+
existing_part = Part.objects.filter(number_class=existing_part_class, number_item=part_dict['number_item'], number_variation=part_dict['number_variation'], organization=self.organization).first()
|
|
1265
|
+
|
|
1266
|
+
existing_part_revision = None
|
|
1267
|
+
if existing_part:
|
|
1268
|
+
existing_part_revision = PartRevision.objects.filter(part=existing_part, revision=part_dict['revision']).first()
|
|
1269
|
+
|
|
1270
|
+
if existing_part_revision and parent_part_revision: # Check for infinite recursion
|
|
1271
|
+
contains_parent = False
|
|
1272
|
+
indented_bom = existing_part_revision.indented()
|
|
1273
|
+
for _, sp in indented_bom.parts.items(): # Make sure the subpart does not contain the parent - infinite recursion!
|
|
1274
|
+
if sp.part_revision == parent_part_revision:
|
|
1275
|
+
contains_parent = True
|
|
1276
|
+
if contains_parent:
|
|
1277
|
+
raise ValidationError(
|
|
1278
|
+
f"Row {row_count} - Uploaded part {part_number} contains parent part in its assembly. Cannot add {part_number} as it would cause infinite recursion. Uploading of this subpart skipped.",
|
|
1279
|
+
code='invalid')
|
|
1280
|
+
|
|
1281
|
+
existing_subpart = None
|
|
1282
|
+
existing_subpart_count = 0
|
|
1283
|
+
existing_subpart_references = None
|
|
1284
|
+
if existing_part_revision and parent_part_revision:
|
|
1285
|
+
existing_subpart = parent_part_revision.assembly.subparts.all().filter(part_revision=existing_part_revision, do_not_load=part_dict['do_not_load']).first()
|
|
1286
|
+
existing_subpart_count = existing_subpart.count if existing_subpart else 0
|
|
1287
|
+
existing_subpart_references = existing_subpart.reference if existing_subpart else None
|
|
1288
|
+
|
|
1289
|
+
# Now validate & save PartClass, Part, PartRevision, Subpart
|
|
1290
|
+
part_class_dict = {'code': part_dict['number_class'], 'name': part_dict.get('part_class', None)}
|
|
1291
|
+
part_class_form = PartClassForm(part_class_dict, instance=existing_part_class, ignore_unique_constraint=True, organization=self.organization)
|
|
1292
|
+
if self.organization.number_scheme == NUMBER_SCHEME_SEMI_INTELLIGENT and not part_class_form.is_valid():
|
|
1293
|
+
add_nonfield_error_from_existing(part_class_form, self, f'Row {row_count} - ')
|
|
1294
|
+
continue
|
|
1295
|
+
|
|
1296
|
+
PartForm = part_form_from_organization(self.organization)
|
|
1297
|
+
part_form = PartForm(part_dict, instance=existing_part, ignore_part_class=True, ignore_unique_constraint=True, organization=self.organization)
|
|
1298
|
+
if not part_form.is_valid():
|
|
1299
|
+
add_nonfield_error_from_existing(part_form, self, f'Row {row_count} - ')
|
|
1300
|
+
continue
|
|
1301
|
+
|
|
1302
|
+
part_revision_form = PartRevisionForm(part_dict, instance=existing_part_revision)
|
|
1303
|
+
if not part_revision_form.is_valid():
|
|
1304
|
+
add_nonfield_error_from_existing(part_revision_form, self, f'Row {row_count} - ')
|
|
1305
|
+
continue
|
|
1306
|
+
|
|
1307
|
+
subpart_form = SubpartForm(part_dict, instance=existing_subpart, ignore_part_revision=True, organization=self.organization)
|
|
1308
|
+
if not subpart_form.is_valid():
|
|
1309
|
+
add_nonfield_error_from_existing(subpart_form, self, f'Row {row_count} - ')
|
|
1310
|
+
continue
|
|
1311
|
+
|
|
1312
|
+
part_class = part_class_form.save(commit=False)
|
|
1313
|
+
part = part_form.save(commit=False)
|
|
1314
|
+
part_revision = part_revision_form.save(commit=False)
|
|
1315
|
+
subpart = subpart_form.save(commit=False)
|
|
1316
|
+
|
|
1317
|
+
reference_list = listify_string(reference) if reference else []
|
|
1318
|
+
if len(reference_list) != len(set(reference_list)):
|
|
1319
|
+
self.add_warning(None, f"Row {row_count} -Duplicate reference designators '{reference}' for subpart on row {row_count}.")
|
|
1320
|
+
if len(reference_list) != subpart.count and len(reference_list) > 0:
|
|
1321
|
+
self.add_warning(None, f"Row {row_count} -The quantity of reference designators for {part_number} on row {row_count} does not match the subpart quantity ({len(reference_list)} != {subpart.count})")
|
|
1322
|
+
|
|
1323
|
+
if self.organization.number_scheme == NUMBER_SCHEME_SEMI_INTELLIGENT:
|
|
1324
|
+
part_class.save()
|
|
1325
|
+
part.number_class = part_class
|
|
1326
|
+
|
|
1327
|
+
part.organization = self.organization
|
|
1328
|
+
part.save()
|
|
1329
|
+
part_revision.part = part
|
|
1330
|
+
part_revision.save()
|
|
1331
|
+
if parent_part_revision:
|
|
1332
|
+
subpart.count += existing_subpart_count # append or create
|
|
1333
|
+
subpart.reference = existing_subpart_references + ', ' + subpart.reference if existing_subpart_references else subpart.reference
|
|
1334
|
+
subpart.part_revision = part_revision
|
|
1335
|
+
subpart.save()
|
|
1336
|
+
AssemblySubparts.objects.get_or_create(assembly=parent_part_revision.assembly, subpart=subpart)
|
|
1337
|
+
|
|
1338
|
+
info_msg = f"Row {row_count}: Added subpart {part_number}"
|
|
1339
|
+
if reference:
|
|
1340
|
+
info_msg += f" with reference designators {reference}"
|
|
1341
|
+
if parent_part_revision:
|
|
1342
|
+
info_msg += f" to parent part {parent_part_revision.part.full_part_number()}"
|
|
1343
|
+
self.successes.append(info_msg + ".")
|
|
1344
|
+
|
|
1345
|
+
# Now validate & save optional fields - Manufacturer, ManufacturerPart, SellerParts
|
|
1346
|
+
existing_manufacturer = Manufacturer.objects.filter(name=manufacturer_name, organization=self.organization).first()
|
|
1347
|
+
manufacturer_form = ManufacturerForm({'name': manufacturer_name}, instance=existing_manufacturer)
|
|
1348
|
+
if not manufacturer_form.is_valid():
|
|
1349
|
+
add_nonfield_error_from_existing(manufacturer_form, self, f'Row {row_count} - ')
|
|
1350
|
+
|
|
1351
|
+
manufacturer_part_data = {'manufacturer_part_number': manufacturer_part_number}
|
|
1352
|
+
manufacturer_part_form = ManufacturerPartForm(manufacturer_part_data)
|
|
1353
|
+
if not manufacturer_part_form.is_valid():
|
|
1354
|
+
add_nonfield_error_from_existing(manufacturer_part_form, self, f'Row {row_count} - ')
|
|
1355
|
+
|
|
1356
|
+
manufacturer = manufacturer_form.save(commit=False)
|
|
1357
|
+
manufacturer.organization = self.organization
|
|
1358
|
+
manufacturer.save()
|
|
1359
|
+
|
|
1360
|
+
manufacturer_part = manufacturer_part_form.save(commit=False)
|
|
1361
|
+
existing_manufacturer_part = ManufacturerPart.objects.filter(part=part, manufacturer=manufacturer, manufacturer_part_number=manufacturer_part.manufacturer_part_number).first()
|
|
1362
|
+
manufacturer_part.id = existing_manufacturer_part.id if existing_manufacturer_part else None
|
|
1363
|
+
manufacturer_part.manufacturer = manufacturer
|
|
1364
|
+
manufacturer_part.part = part
|
|
1365
|
+
manufacturer_part.save()
|
|
1366
|
+
|
|
1367
|
+
part.primary_manufacturer_part = manufacturer_part
|
|
1368
|
+
part.save()
|
|
1369
|
+
|
|
1370
|
+
last_part_revision = part_revision
|
|
1371
|
+
last_level = level
|
|
1372
|
+
|
|
1373
|
+
# TODO: Add SellerParts
|
|
1374
|
+
except UnicodeDecodeError as e:
|
|
1375
|
+
self.add_error(None, forms.ValidationError("CSV File Encoding error, try encoding your file as utf-8, and upload again. \
|
|
1376
|
+
If this keeps happening, reach out to info@indabom.com with your csv file and we'll do our best to \
|
|
1377
|
+
fix your issue!", code='invalid'))
|
|
1378
|
+
logger.warning("UnicodeDecodeError: {}".format(e))
|
|
1379
|
+
raise ValidationError("Specific Error: {}".format(e), code='invalid')
|
|
1380
|
+
|
|
1381
|
+
return cleaned_data
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
class FileForm(forms.Form):
|
|
1385
|
+
file = forms.FileField()
|
|
1386
|
+
|
|
1387
|
+
def part_form_from_organization(organization):
|
|
1388
|
+
return PartFormSemiIntelligent if organization.number_scheme == NUMBER_SCHEME_SEMI_INTELLIGENT else PartFormIntelligent
|
|
1389
|
+
|
|
1390
|
+
def add_nonfield_error_from_existing(from_form, to_form, prefix=''):
|
|
1391
|
+
for field, errors in from_form.errors.as_data().items():
|
|
1392
|
+
for error in errors:
|
|
1393
|
+
for msg in error.messages:
|
|
1394
|
+
to_form.add_error(None, f'{prefix}{field}: {msg}')
|
|
1395
|
+
|
|
1396
|
+
def add_nonfield_warning_from_existing(from_form, to_form, prefix=''):
|
|
1397
|
+
for field, errors in from_form.errors.as_data().items():
|
|
1398
|
+
for error in errors:
|
|
1399
|
+
for msg in error.messages:
|
|
1400
|
+
to_form.add_warning(None, f'{prefix}{field}: {msg}')
|