django-bom 1.243__py3-none-any.whl → 1.257__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.
@@ -7,7 +7,7 @@
7
7
 
8
8
  {% block content %}
9
9
  <div class="row container-app">
10
- <div class="col s8 offset-s2">
10
+ <div class="col s12 l8 offset-l2">
11
11
  <ul id="tabs" class="tabs tabs-fixed-width">
12
12
  <li class="tab"><a id="user-tab" href="#user">User</a></li>
13
13
  <li class="tab"><a id="indabom-tab" href="#indabom">IndaBOM</a></li>
@@ -45,10 +45,9 @@
45
45
  </form>
46
46
  </div>
47
47
 
48
- <div class="section"
49
- style="background: #ffebee; border: 1px solid #ffcdd2; padding: 16px; border-radius: 6px;">
50
- <h5 class="section-title red-text text-darken-2" style="margin-top:0;"><i
51
- class="material-icons red-text text-darken-2">warning</i>Danger Zone</h5>
48
+ <div class="section" style="padding: 16px; border-radius: 6px;">
49
+ <h4 class="section-title red-text text-darken-2" style="margin-top:0;"><i
50
+ class="material-icons red-text text-darken-2">warning</i>Danger Zone</h4>
52
51
  <p>Deleting your account is permanent. If you are the organization owner, your organization will also be
53
52
  deleted unless you transfer ownership before proceeding.</p>
54
53
  <div class="right-align" style="margin-top: 8px;">
@@ -120,7 +119,7 @@
120
119
  title="Via Mouser.com"
121
120
  src="{% static 'bom/img/mouser.png' %}">{% endif %}</td>
122
121
  <td>
123
- <a class="waves-effect btn-flat"
122
+ <a class="table-action"
124
123
  href="{% url 'bom:part-class-edit' part_class_id=part_class.id %}"><i
125
124
  class="material-icons left">edit</i>Edit</a>
126
125
  </td>
@@ -144,7 +143,134 @@
144
143
  </div>
145
144
 
146
145
  <div class="divider"></div>
146
+ {% endif %}
147
+
148
+ <div class="section">
149
+ <h4 class="section-title"><i class="material-icons teal-text text-darken-1">list_alt</i>Property
150
+ Definitions</h4>
151
+ <p>Define part properties (e.g. Capacitance, Length, Sheen), their data type, and their
152
+ associated quantity of measure.</p>
153
+ {% if property_definitions.count > 0 %}
154
+ <table class="striped tight">
155
+ <thead>
156
+ <tr>
157
+ <th class="text-normal">Name</th>
158
+ <th class="text-normal">Type</th>
159
+ <th class="text-normal">Required</th>
160
+ <th class="text-normal">Quantity of Measure</th>
161
+ <th class="text-normal">Options</th>
162
+ </tr>
163
+ </thead>
164
+ <tbody>
165
+ {% for prop in property_definitions %}
166
+ <tr>
167
+ <td class="text-normal">{{ prop.name }}</td>
168
+ <td class="text-normal">{{ prop.get_type_display }}</td>
169
+ <td class="text-normal">{% if prop.required %}Yes{% else %}No{% endif %}</td>
170
+ <td class="text-normal">{{ prop.quantity_of_measure.name|default:"-" }}</td>
171
+ <td>
172
+ {% if prop.organization == organization %}
173
+ <a class="table-action"
174
+ href="{% url 'bom:property-definition-edit' property_definition_id=prop.id %}"><i
175
+ class="material-icons left">edit</i>Edit</a>
176
+ <a class="table-action red-text"
177
+ href="{% url 'bom:property-definition-delete' property_definition_id=prop.id %}"
178
+ onclick="return confirm('Are you sure you want to delete this property definition?')"><i
179
+ class="material-icons left">delete</i></a>
180
+ {% else %}
181
+ <span class="table-action tooltipped grey-text d-flex-inline align-center"
182
+ data-position="top"
183
+ data-tooltip="System Default: Cannot be modified">
184
+ <i class="material-icons left" style="font-size: 1.2rem;">lock</i> System
185
+ </span>
186
+ {% endif %}
187
+ </td>
188
+ </tr>
189
+ {% endfor %}
190
+ </tbody>
191
+ </table>
192
+ {% else %}
193
+ <p>No global property definitions defined.</p>
194
+ {% endif %}
195
+ <div class="right-align" style="margin-top: 16px;">
196
+ <a href="{% url 'bom:property-definition-add' %}"
197
+ class="waves-effect waves-light btn btn-primary">
198
+ <i class="material-icons left">add</i>Add Property Definition
199
+ </a>
200
+ </div>
201
+ </div>
202
+
203
+ <div class="divider"></div>
204
+
205
+ <div class="section">
206
+ <h4 class="section-title"><i class="material-icons teal-text text-darken-1">straighten</i>Units &
207
+ Quantities</h4>
208
+ <p>Define quantities of measure (e.g. Length, Resistance) and their associated units (e.g. m, mm,
209
+ Ohm, kOhm).</p>
210
+ {% if quantities_of_measure.count > 0 %}
211
+ <table class="striped tight">
212
+ <thead>
213
+ <tr>
214
+ <th class="text-normal">Quantity</th>
215
+ <th class="text-normal">Units</th>
216
+ <th class="text-normal">Options</th>
217
+ </tr>
218
+ </thead>
219
+ <tbody>
220
+ {% for qom in quantities_of_measure %}
221
+ <tr>
222
+ <td class="text-normal">{{ qom.name }}</td>
223
+ <td class="text-normal">
224
+ {% for unit in qom.units.all|slice:":3" %}
225
+ {{ unit.symbol }}{% if not forloop.last %}, {% endif %}
226
+ {% endfor %}
227
+
228
+ {% if qom.units.count > 3 %}
229
+ <span class="grey-text text-darken-1 tooltipped"
230
+ data-tooltip="
231
+
147
232
 
233
+
234
+
235
+ {% for unit in qom.units.all %}{{ unit.name }}{% if not forloop.last %}, {% endif %}{% endfor %}">
236
+ (+{{ qom.units.count|add:"-3" }} more)
237
+ </span>
238
+ {% endif %}
239
+ </td>
240
+ <td>
241
+ {% if qom.organization == organization %}
242
+ <a class="table-action"
243
+ href="{% url 'bom:quantity-of-measure-edit' quantity_of_measure_id=qom.id %}"><i
244
+ class="material-icons left">edit</i>Edit</a>
245
+ <a class="table-action red-text"
246
+ href="{% url 'bom:quantity-of-measure-delete' quantity_of_measure_id=qom.id %}"
247
+ onclick="return confirm('Are you sure you want to delete this quantity of measure and all its units?')"><i
248
+ class="material-icons left">delete</i></a>
249
+ {% else %}
250
+ <span class="table-action tooltipped grey-text d-flex-inline align-center"
251
+ data-position="top"
252
+ data-tooltip="System Default: Cannot be modified">
253
+ <i class="material-icons left" style="font-size: 1.2rem;">lock</i> System
254
+ </span>
255
+ {% endif %}
256
+ </td>
257
+ </tr>
258
+ {% endfor %}
259
+ </tbody>
260
+ </table>
261
+ {% else %}
262
+ <p>No quantities of measure defined.</p>
263
+ {% endif %}
264
+ <div class="right-align" style="margin-top: 16px;">
265
+ <a href="{% url 'bom:quantity-of-measure-add' %}"
266
+ class="waves-effect waves-light btn btn-primary">
267
+ <i class="material-icons left">add</i>Add Quantity of Measure
268
+ </a>
269
+ </div>
270
+ </div>
271
+
272
+ {% if organization.number_scheme == 'S' %}
273
+ <div class="divider"></div>
148
274
  <div class="section">
149
275
  <h4 class="section-title" id="indabom-part-number"><i
150
276
  class="material-icons teal-text text-darken-1">format_list_numbered</i>Part Number</h4>
@@ -242,6 +368,9 @@
242
368
  </form>
243
369
  </div>
244
370
 
371
+ {# Subscription & Billing placed after Users for a more natural flow #}
372
+ {% include 'bom/subscription_panel.html' %}
373
+
245
374
  <div class="section">
246
375
  <h4 class="section-title"><i class="material-icons teal-text text-darken-1">group</i>Users</h4>
247
376
  <form name="seller" action="{% url 'bom:settings' tab_anchor=ORGANIZATION_TAB %}" method="post"
@@ -263,17 +392,18 @@
263
392
  {% for org_user in users_in_organization %}
264
393
  <tr>
265
394
  <td>
266
- <label><input type="checkbox" class="filled-in"
267
- name="remove_user_meta_id_{{ org_user.bom_profile.id }}"><span/></label>
395
+ {% if org_user != organization.owner %}
396
+ <label><input type="checkbox" class="filled-in"
397
+ name="remove_user_meta_id_{{ org_user.bom_profile.id }}"><span/></label>{% endif %}
268
398
  </td>
269
- <td class="text-normal">{{ org_user.bom_profile.get_role_display }}</td>
399
+ <td class="text-normal">{% if org_user == organization.owner %}Owner{% else %}
400
+ {{ org_user.bom_profile.get_role_display }}{% endif %}</td>
270
401
  <td class="text-normal">{{ org_user.username }}</td>
271
402
  <td class="text-normal">{{ org_user.first_name }} {{ org_user.last_name }}</td>
272
403
  <td class="text-normal"><a href="mailto:{{ user.email }}">{{ org_user.email }}</a>
273
404
  </td>
274
- <td>
275
- <a class="waves-effect btn-flat"
276
- href="{% url 'bom:user-meta-edit' user_meta_id=org_user.bom_profile.id %}"><i
405
+ <td class="table-action right-align">
406
+ <a href="{% url 'bom:user-meta-edit' user_meta_id=org_user.bom_profile.id %}"><i
277
407
  class="material-icons left">edit</i>Edit</a>
278
408
  </td>
279
409
  </tr>
@@ -301,9 +431,6 @@
301
431
  </form>
302
432
  </div>
303
433
 
304
- {# Subscription & Billing placed after Users for a more natural flow #}
305
- {% include 'bom/subscription_panel.html' %}
306
-
307
434
  <div class="section">
308
435
  <h4 class="section-title"><i
309
436
  class="material-icons teal-text text-darken-1">integration_instructions</i>Integrations</h4>
bom/tests.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import csv
2
- from re import finditer, search
2
+ from re import finditer
3
3
  from unittest import skip
4
4
 
5
5
  from django.conf import settings
@@ -16,12 +16,11 @@ from .helpers import (
16
16
  create_a_fake_subpart,
17
17
  create_some_fake_manufacturers,
18
18
  create_some_fake_part_classes,
19
+ create_some_fake_part_revision_property_definitions,
19
20
  create_some_fake_parts,
20
- create_some_fake_sellers,
21
21
  create_user_and_organization,
22
22
  )
23
- from .models import ManufacturerPart, Part, PartClass, Seller, SellerPart, Subpart
24
-
23
+ from .models import Part, PartClass, Seller, Subpart
25
24
 
26
25
  TEST_FILES_DIR = "bom/test_files"
27
26
 
@@ -186,6 +185,7 @@ class TestBOM(TransactionTestCase):
186
185
  self.assertEqual(response.status_code, 200)
187
186
 
188
187
  def test_part_upload_bom(self):
188
+ sheen, voltage, _, _, _ = create_some_fake_part_revision_property_definitions(self.organization, False)
189
189
  (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
190
190
 
191
191
  test_file = 'test_bom.csv' if self.organization.number_variation_len > 0 else 'test_bom_6_no_variations.csv'
@@ -195,7 +195,7 @@ class TestBOM(TransactionTestCase):
195
195
 
196
196
  messages = list(response.context.get('messages'))
197
197
  for msg in messages:
198
- self.assertNotEqual(msg.tags, "error")
198
+ self.assertNotEqual(msg.tags, "error", msg.message)
199
199
 
200
200
  subparts = p2.latest().assembly.subparts.all()
201
201
 
@@ -227,7 +227,21 @@ class TestBOM(TransactionTestCase):
227
227
  bom = p1.latest().indented()
228
228
  self.assertEqual(len(bom.parts), 3)
229
229
 
230
+ def test_part_upload_bom_with_properties(self):
231
+ sheen, voltage, _, _, _ = create_some_fake_part_revision_property_definitions(self.organization)
232
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
233
+
234
+ with open(f'{TEST_FILES_DIR}/test_bom_7_properties.csv') as test_csv:
235
+ response = self.client.post(reverse('bom:part-upload-bom', kwargs={'part_id': p2.id}), {'file': test_csv},
236
+ follow=True)
237
+ self.assertEqual(response.status_code, 200)
238
+
239
+ messages = list(response.context.get('messages'))
240
+ for msg in messages:
241
+ self.assertNotEqual(msg.tags, "error", msg.message)
242
+
230
243
  def test_upload_bom(self):
244
+ create_some_fake_part_revision_property_definitions(self.organization, some_required=False)
231
245
  (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
232
246
 
233
247
  # Test OK page visit
@@ -253,7 +267,8 @@ class TestBOM(TransactionTestCase):
253
267
  parent_part = Part.from_part_number(parent_part_number, organization=self.organization)
254
268
  bom = parent_part.indented()
255
269
  bom_list = list(bom.parts.values())
256
- self.assertEqual(len(bom.parts), len(test_list))
270
+ for idx, row in enumerate(test_list):
271
+ self.assertEqual(row['part_number'], bom_list[idx].part.full_part_number(), f'Row {idx + 1}')
257
272
 
258
273
  # Check that we successfully updated an existing part (only tested for semi-intelligent scheme for now)
259
274
  if self.organization.number_scheme == constants.NUMBER_SCHEME_SEMI_INTELLIGENT:
@@ -330,7 +345,17 @@ class TestBOM(TransactionTestCase):
330
345
  self.assertTrue("it would cause infinite recursion. Uploading of this subpart skipped." in str(msg.message))
331
346
  self.assertTrue("Row 15" in str(msg.message))
332
347
 
348
+ def test_upload_bom_with_properties(self):
349
+ (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
350
+
351
+ # Test OK upload
352
+ test_file = 'test_full_bom.csv'
353
+ with open(f'{TEST_FILES_DIR}/{test_file}') as test_csv:
354
+ response = self.client.post(reverse('bom:upload-bom'), {'file': test_csv}, follow=True)
355
+ self.assertEqual(response.status_code, 200)
356
+
333
357
  def test_part_upload_bom_corner_cases(self):
358
+ create_some_fake_part_revision_property_definitions(self.organization, False)
334
359
  (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
335
360
  with open(f'{TEST_FILES_DIR}/test_bom_3_recursion.csv') as test_csv:
336
361
  response = self.client.post(reverse('bom:part-upload-bom', kwargs={'part_id': p1.id}), {'file': test_csv}, follow=True)
@@ -347,7 +372,7 @@ class TestBOM(TransactionTestCase):
347
372
 
348
373
  messages = list(response.context.get('messages'))
349
374
  for msg in messages:
350
- self.assertNotEqual(msg.tags, "error") # Should be OK since we will default revision to 1
375
+ self.assertNotEqual(msg.tags, "error", msg.message) # Should be OK since we will default revision to 1
351
376
 
352
377
  def test_export_part_list(self):
353
378
  create_some_fake_parts(organization=self.organization)
@@ -371,14 +396,37 @@ class TestBOM(TransactionTestCase):
371
396
  self.assertEqual(part_classes.count(), 1)
372
397
  part_class = part_classes[0]
373
398
 
374
- # Test edit
399
+ # Test edit with property definitions
400
+ _, prop_def, _, _, _ = create_some_fake_part_revision_property_definitions(self.organization, False)
375
401
  part_class_form_data['name'] = 'edited test part name'
402
+ part_class_form_data.update({
403
+ 'prop-def-TOTAL_FORMS': '1',
404
+ 'prop-def-INITIAL_FORMS': '0',
405
+ 'prop-def-MIN_NUM_FORMS': '0',
406
+ 'prop-def-MAX_NUM_FORMS': '1000',
407
+ 'prop-def-0-property_definition': prop_def.id,
408
+ })
376
409
 
377
410
  response = self.client.post(reverse('bom:part-class-edit', kwargs={'part_class_id': part_class.id}), part_class_form_data)
378
411
  self.assertEqual(response.status_code, 302)
379
412
 
380
- part_class = PartClass.objects.get(id=part_class.id)
413
+ part_class.refresh_from_db()
381
414
  self.assertEqual(part_class.name, part_class_form_data['name'])
415
+ self.assertEqual(part_class.property_definitions.count(), 1)
416
+ prop_def = part_class.property_definitions.first()
417
+ self.assertEqual(prop_def.name, 'Voltage')
418
+
419
+ # Test deleting property definition
420
+ part_class_form_data.update({
421
+ 'prop-def-INITIAL_FORMS': '1',
422
+ 'prop-def-0-property_definition': prop_def.id,
423
+ 'prop-def-0-DELETE': 'on',
424
+ })
425
+ response = self.client.post(reverse('bom:part-class-edit', kwargs={'part_class_id': part_class.id}),
426
+ part_class_form_data)
427
+ self.assertEqual(response.status_code, 302)
428
+ part_class.refresh_from_db()
429
+ self.assertEqual(part_class.property_definitions.count(), 0)
382
430
 
383
431
  def test_create_part(self):
384
432
  (p1, p2, p3, p4) = create_some_fake_parts(organization=self.organization)
@@ -393,8 +441,8 @@ class TestBOM(TransactionTestCase):
393
441
  'configuration': 'W',
394
442
  'description': 'IC, MCU 32 Bit',
395
443
  'revision': 'A',
396
- 'attribute': '',
397
- 'value': ''
444
+ 'property_sheen': 'flat',
445
+ 'property_voltage': '1.34',
398
446
  }
399
447
 
400
448
  response = self.client.post(reverse('bom:create-part'), new_part_form_data)
@@ -417,6 +465,8 @@ class TestBOM(TransactionTestCase):
417
465
  'number_item': '9999',
418
466
  'description': 'IC, MCU 32 Bit',
419
467
  'revision': 'A',
468
+ 'property_sheen': 'flat',
469
+ 'property_voltage': '1.34',
420
470
  }
421
471
 
422
472
  if self.organization.number_variation_len > 0:
@@ -434,6 +484,8 @@ class TestBOM(TransactionTestCase):
434
484
  'number_variation': '',
435
485
  'description': 'IC, MCU 32 Bit',
436
486
  'revision': 'A',
487
+ 'property_sheen': 'flat',
488
+ 'property_voltage': '1.34',
437
489
  }
438
490
 
439
491
  response = self.client.post(reverse('bom:create-part'), new_part_form_data)
@@ -447,6 +499,8 @@ class TestBOM(TransactionTestCase):
447
499
  'number_item': '1234',
448
500
  'description': 'IC, MCU 32 Bit',
449
501
  'revision': 'A',
502
+ 'property_sheen': 'flat',
503
+ 'property_voltage': '1.34',
450
504
  }
451
505
 
452
506
  if self.organization.number_variation_len > 0:
@@ -464,6 +518,8 @@ class TestBOM(TransactionTestCase):
464
518
  'number_variation': '',
465
519
  'description': 'IC, MCU 32 Bit',
466
520
  'revision': 'A',
521
+ 'property_sheen': 'flat',
522
+ 'property_voltage': '1.34',
467
523
  }
468
524
 
469
525
  response = self.client.post(reverse('bom:create-part'), new_part_form_data)
@@ -479,6 +535,8 @@ class TestBOM(TransactionTestCase):
479
535
  'number_variation': '',
480
536
  'description': 'IC, MCU 32 Bit',
481
537
  'revision': 'A',
538
+ 'property_sheen': 'flat',
539
+ 'property_voltage': '1.34',
482
540
  }
483
541
 
484
542
  response = self.client.post(reverse('bom:create-part'), new_part_form_data)
@@ -506,8 +564,8 @@ class TestBOM(TransactionTestCase):
506
564
  'configuration': 'W',
507
565
  'description': 'IC, MCU 32 Bit',
508
566
  'revision': 'A',
509
- 'attribute': '',
510
- 'value': ''
567
+ 'property_sheen': 'flat',
568
+ 'property_voltage': '1.34',
511
569
  }
512
570
 
513
571
  response = self.client.post(reverse('bom:create-part'), new_part_form_data)
@@ -535,8 +593,8 @@ class TestBOM(TransactionTestCase):
535
593
  'configuration': 'W',
536
594
  'description': 'IC, MCU 32 Bit',
537
595
  'revision': 'A',
538
- 'attribute': '',
539
- 'value': ''
596
+ 'property_sheen': 'flat',
597
+ 'property_voltage': '1.34',
540
598
  }
541
599
 
542
600
  number_variation = None
@@ -709,17 +767,20 @@ class TestBOM(TransactionTestCase):
709
767
  found_error = True
710
768
  self.assertTrue(found_error)
711
769
 
712
- def test_upload_parts_break_tolerance(self):
713
- create_some_fake_part_classes(self.organization)
770
+ def test_upload_parts_break_too_many_characters(self):
771
+ pc1, _, _ = create_some_fake_part_classes(self.organization)
772
+ create_some_fake_part_revision_property_definitions(self.organization, part_class=pc1)
714
773
 
715
774
  # Should break with data error
716
775
  with open(f'{TEST_FILES_DIR}/test_new_parts_broken.csv') as test_csv:
717
776
  response = self.client.post(reverse('bom:upload-parts'), {'file': test_csv}, follow=True)
718
777
  messages = list(response.context.get('messages'))
719
778
 
720
- self.assertTrue(len(messages) > 0)
721
- for msg in messages:
722
- self.assertEqual(msg.tags, 'error')
779
+ self.assertTrue(len(messages) == 1)
780
+ msg = messages[0]
781
+ self.assertEqual(msg.tags, 'error', msg.message)
782
+ self.assertIn('Error on Row 2, property_sheen: Ensure this value has at most 255 characters (it has 483)',
783
+ msg.message)
723
784
 
724
785
  def test_upload_part_with_sellers(self):
725
786
  create_some_fake_part_classes(self.organization)
@@ -750,14 +811,14 @@ class TestBOM(TransactionTestCase):
750
811
  with open(f'{TEST_FILES_DIR}/test_part_classes_no_comment.csv') as test_csv:
751
812
  response = self.client.post(reverse('bom:settings'), {'file': test_csv, 'submit-part-class-upload': ''})
752
813
  self.assertEqual(response.status_code, 200)
753
- self.assertTrue('Part class 102 Resistor on row 3 is already defined. Uploading of this part class skipped.' in str(response.content))
814
+ self.assertTrue('Row 3: Part class 102 Resistor already defined.' in str(response.content))
754
815
 
755
816
  # Submit with a weird csv file that sort of works
756
817
  with open(f'{TEST_FILES_DIR}/test_part_classes_blank_rows.csv') as test_csv:
757
818
  response = self.client.post(reverse('bom:settings'), {'file': test_csv, 'submit-part-class-upload': ''})
758
819
  self.assertEqual(response.status_code, 200)
759
- self.assertTrue('in row 3 does not have a value. Uploading of this part class skipped.' in str(response.content))
760
- self.assertTrue('in row 4 does not have a value. Uploading of this part class skipped.' in str(response.content))
820
+ self.assertTrue('Row 3: Missing code.' in str(response.content))
821
+ self.assertTrue('Row 4: Missing code.' in str(response.content))
761
822
 
762
823
  # Submit with a csv file exported with a byte order mask, typically from MS word I think
763
824
  with open(f'{TEST_FILES_DIR}/test_part_classes_byte_order.csv') as test_csv:
@@ -1033,7 +1094,9 @@ class TestBOM(TransactionTestCase):
1033
1094
  'value': '10k',
1034
1095
  'part': p1.id,
1035
1096
  'configuration': 'W',
1036
- 'copy_assembly': 'False'
1097
+ 'copy_assembly': 'False',
1098
+ 'property_sheen': 'flat',
1099
+ 'property_voltage': '1.34',
1037
1100
  }
1038
1101
 
1039
1102
  response = self.client.post(
@@ -1048,7 +1111,9 @@ class TestBOM(TransactionTestCase):
1048
1111
  'revision': '5',
1049
1112
  'part': p3.id,
1050
1113
  'configuration': 'W',
1051
- 'copy_assembly': 'true'
1114
+ 'copy_assembly': 'true',
1115
+ 'property_sheen': 'flat',
1116
+ 'property_voltage': '1.34',
1052
1117
  }
1053
1118
 
1054
1119
  response = self.client.post(
@@ -1077,7 +1142,9 @@ class TestBOM(TransactionTestCase):
1077
1142
  'revision': '4',
1078
1143
  'attribute': 'resistance',
1079
1144
  'value': '10k',
1080
- 'part': p1.id
1145
+ 'part': p1.id,
1146
+ 'property_sheen': 'Flat',
1147
+ 'property_voltage': '0',
1081
1148
  }
1082
1149
 
1083
1150
  response = self.client.post(
@@ -1114,8 +1181,8 @@ class TestBOMIntelligent(TestBOM):
1114
1181
  'configuration': 'W',
1115
1182
  'description': 'IC, MCU 32 Bit',
1116
1183
  'revision': 'A',
1117
- 'attribute': '',
1118
- 'value': ''
1184
+ 'property_sheen': 'flat',
1185
+ 'property_voltage': '1.34',
1119
1186
  }
1120
1187
 
1121
1188
  response = self.client.post(reverse('bom:create-part'), new_part_form_data)
@@ -1137,6 +1204,8 @@ class TestBOMIntelligent(TestBOM):
1137
1204
  'number_item': '9999',
1138
1205
  'description': 'IC, MCU 32 Bit',
1139
1206
  'revision': 'A',
1207
+ 'property_sheen': 'flat',
1208
+ 'property_voltage': '1.34',
1140
1209
  }
1141
1210
 
1142
1211
  response = self.client.post(reverse('bom:create-part'), new_part_form_data)
@@ -1149,6 +1218,8 @@ class TestBOMIntelligent(TestBOM):
1149
1218
  'number_item': '5432',
1150
1219
  'description': 'IC, MCU 32 Bit',
1151
1220
  'revision': 'A',
1221
+ 'property_sheen': 'flat',
1222
+ 'property_voltage': '1.34',
1152
1223
  }
1153
1224
 
1154
1225
  response = self.client.post(reverse('bom:create-part'), new_part_form_data)
@@ -1161,6 +1232,8 @@ class TestBOMIntelligent(TestBOM):
1161
1232
  'number_item': '1234A',
1162
1233
  'description': 'IC, MCU 32 Bit',
1163
1234
  'revision': 'A',
1235
+ 'property_sheen': 'flat',
1236
+ 'property_voltage': '1.34',
1164
1237
  }
1165
1238
 
1166
1239
  response = self.client.post(reverse('bom:create-part'), new_part_form_data)
@@ -1173,6 +1246,8 @@ class TestBOMIntelligent(TestBOM):
1173
1246
  'number_item': '1235',
1174
1247
  'description': 'IC, MCU 32 Bit',
1175
1248
  'revision': 'A',
1249
+ 'property_sheen': 'flat',
1250
+ 'property_voltage': '1.34',
1176
1251
  }
1177
1252
 
1178
1253
  response = self.client.post(reverse('bom:create-part'), new_part_form_data)
@@ -1186,6 +1261,8 @@ class TestBOMIntelligent(TestBOM):
1186
1261
  'number_item': p1.number_item,
1187
1262
  'description': 'IC, MCU 32 Bit',
1188
1263
  'revision': 'A',
1264
+ 'property_sheen': 'flat',
1265
+ 'property_voltage': '1.34',
1189
1266
  }
1190
1267
 
1191
1268
  response = self.client.post(reverse('bom:create-part'), new_part_form_data)
@@ -1214,8 +1291,8 @@ class TestBOMIntelligent(TestBOM):
1214
1291
  'configuration': 'W',
1215
1292
  'description': 'IC, MCU 32 Bit',
1216
1293
  'revision': 'A',
1217
- 'attribute': '',
1218
- 'value': ''
1294
+ 'property_sheen': 'flat',
1295
+ 'property_voltage': '1.34',
1219
1296
  }
1220
1297
 
1221
1298
  response = self.client.post(reverse('bom:create-part'), new_part_form_data)
@@ -1252,7 +1329,7 @@ class TestBOMIntelligent(TestBOM):
1252
1329
 
1253
1330
  messages = list(response.context.get('messages'))
1254
1331
  for msg in messages:
1255
- self.assertNotEqual(msg.tags, "error") # Error loading 200-3333-00 via CSV because already in parent's BOM and has empty ref designators
1332
+ self.assertNotEqual(msg.tags, "error", msg.message)
1256
1333
 
1257
1334
  subparts = p2.latest().assembly.subparts.all()
1258
1335
 
@@ -1382,6 +1459,15 @@ class TestBOMNoVariation(TestBOM):
1382
1459
  def test_part_upload_bom_corner_cases(self):
1383
1460
  pass
1384
1461
 
1462
+ @skip('too specific of a test case for now...')
1463
+ def test_part_upload_bom_with_properties(self):
1464
+ pass
1465
+
1466
+ @skip('too specific of a test case for now...')
1467
+ def test_upload_parts_break_too_many_characters(self):
1468
+ pass
1469
+
1470
+
1385
1471
  @override_settings(BOM_CONFIG=settings.BOM_CONFIG_DEFAULT)
1386
1472
  class TestForms(TestCase):
1387
1473
  def setUp(self):
bom/urls.py CHANGED
@@ -31,6 +31,12 @@ bom_patterns = [
31
31
  path('export/', views.export_part_list, name='export-part-list'),
32
32
  path('user-meta/<int:user_meta_id>/edit/', views.user_meta_edit, name='user-meta-edit'),
33
33
  path('part-class/<int:part_class_id>/edit/', views.part_class_edit, name='part-class-edit'),
34
+ path('property-definition/add/', views.property_definition_edit, name='property-definition-add'),
35
+ path('property-definition/<int:property_definition_id>/edit/', views.property_definition_edit, name='property-definition-edit'),
36
+ path('property-definition/<int:property_definition_id>/delete/', views.property_definition_delete, name='property-definition-delete'),
37
+ path('quantity-of-measure/add/', views.quantity_of_measure_edit, name='quantity-of-measure-add'),
38
+ path('quantity-of-measure/<int:quantity_of_measure_id>/edit/', views.quantity_of_measure_edit, name='quantity-of-measure-edit'),
39
+ path('quantity-of-measure/<int:quantity_of_measure_id>/delete/', views.quantity_of_measure_delete, name='quantity-of-measure-delete'),
34
40
  path('create-part/', views.create_part, name='create-part'),
35
41
  path('upload-parts/', views.upload_parts, name='upload-parts'),
36
42
  path('upload-parts-help/', views.upload_parts_help, name='upload-parts-help'),
bom/views/json_views.py CHANGED
@@ -5,9 +5,9 @@ from django.shortcuts import get_object_or_404
5
5
  from django.utils.decorators import method_decorator
6
6
  from django.views import View
7
7
 
8
- from bom.models import Part, PartClass, Subpart, SellerPart, Organization, Manufacturer, ManufacturerPart, User, UserMeta, PartRevision, Assembly, AssemblySubparts
9
- from bom.third_party_apis.mouser import Mouser
8
+ from bom.models import PartRevision
10
9
  from bom.third_party_apis.base_api import BaseApiError
10
+ from bom.third_party_apis.mouser import Mouser
11
11
 
12
12
 
13
13
  class BomJsonResponse(View):