django-bom 1.252__py3-none-any.whl → 1.258__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.
bom/models.py CHANGED
@@ -1,24 +1,28 @@
1
1
  from __future__ import unicode_literals
2
2
 
3
3
  import logging
4
+ from decimal import Decimal, InvalidOperation
4
5
 
5
6
  from django.apps import apps
6
7
  from django.conf import settings
7
8
  from django.contrib.auth import get_user_model
8
9
  from django.core.cache import cache
10
+ from django.core.exceptions import ValidationError
9
11
  from django.core.validators import MaxValueValidator, MinValueValidator
10
12
  from django.db import models
11
13
  from django.utils import timezone
14
+ from django.utils.text import slugify
12
15
  from djmoney.models.fields import CURRENCY_CHOICES, CurrencyField, MoneyField
13
16
  from math import ceil
14
17
  from social_django.models import UserSocialAuth
15
18
 
16
19
  from .base_classes import AsDictModel
17
20
  from .constants import *
18
- from .csv_headers import PartsListCSVHeaders, PartsListCSVHeadersSemiIntelligent
21
+ from .csv_headers import PartsListCSVHeaders, PartsListCSVHeadersSemiIntelligent, BOMIndentedCSVHeaders, \
22
+ BOMFlatCSVHeaders
19
23
  from .part_bom import PartBom, PartBomItem, PartIndentedBomItem
20
24
  from .utils import increment_str, listify_string, prep_for_sorting_nicely, stringify_list, strip_trailing_zeros
21
- from .validators import alphanumeric, validate_pct
25
+ from .validators import alphanumeric
22
26
 
23
27
  logger = logging.getLogger(__name__)
24
28
  User = get_user_model()
@@ -47,9 +51,29 @@ def _user_meta(self, organization=None):
47
51
  return meta
48
52
 
49
53
 
54
+ class OrganizationManager(models.Manager):
55
+ def available_to(self, organization):
56
+ return self.get_queryset().filter(
57
+ models.Q(organization=organization) |
58
+ models.Q(organization__isnull=True)
59
+ )
60
+
61
+
50
62
  class OrganizationScopedModel(models.Model):
51
63
  organization = models.ForeignKey(settings.BOM_ORGANIZATION_MODEL, on_delete=models.CASCADE, db_index=True)
52
64
 
65
+ objects = OrganizationManager()
66
+
67
+ class Meta:
68
+ abstract = True
69
+
70
+
71
+ class OrganizationOptionalModel(models.Model):
72
+ organization = models.ForeignKey(settings.BOM_ORGANIZATION_MODEL, on_delete=models.CASCADE, db_index=True,
73
+ null=True, blank=True, help_text="Leave empty for a Global/System record.")
74
+
75
+ objects = OrganizationManager()
76
+
53
77
  class Meta:
54
78
  abstract = True
55
79
 
@@ -90,9 +114,26 @@ class AbstractOrganization(models.Model):
90
114
 
91
115
  def part_list_csv_headers(self):
92
116
  if self.number_scheme == NUMBER_SCHEME_INTELLIGENT:
93
- return PartsListCSVHeaders()
117
+ headers = PartsListCSVHeaders()
94
118
  else:
95
- return PartsListCSVHeadersSemiIntelligent()
119
+ headers = PartsListCSVHeadersSemiIntelligent()
120
+
121
+ # Add dynamic headers
122
+ definitions = PartRevisionPropertyDefinition.objects.available_to(self).all()
123
+ headers.add_dynamic_headers(definitions)
124
+ return headers
125
+
126
+ def bom_indented_csv_headers(self):
127
+ headers = BOMIndentedCSVHeaders()
128
+ definitions = PartRevisionPropertyDefinition.objects.available_to(self).all()
129
+ headers.add_dynamic_headers(definitions)
130
+ return headers
131
+
132
+ def bom_flat_csv_headers(self):
133
+ headers = BOMFlatCSVHeaders()
134
+ definitions = PartRevisionPropertyDefinition.objects.available_to(self).all()
135
+ headers.add_dynamic_headers(definitions)
136
+ return headers
96
137
 
97
138
  @property
98
139
  def email(self):
@@ -160,6 +201,8 @@ class PartClass(OrganizationScopedModel):
160
201
  name = models.CharField(max_length=255, default=None)
161
202
  comment = models.CharField(max_length=255, default='', blank=True)
162
203
  mouser_enabled = models.BooleanField(default=False)
204
+ property_definitions = models.ManyToManyField('PartRevisionPropertyDefinition', blank=True,
205
+ related_name='part_classes')
163
206
 
164
207
  class Meta(OrganizationScopedModel.Meta):
165
208
  unique_together = [['code', 'organization', ], ]
@@ -329,7 +372,7 @@ class Part(OrganizationScopedModel):
329
372
 
330
373
  def manufacturer_parts(self, exclude_primary=False):
331
374
  q = ManufacturerPart.objects.filter(part=self).select_related('manufacturer')
332
- if exclude_primary and self.primary_manufacturer_part is not None and self.primary_manufacturer_part.optimal_seller():
375
+ if exclude_primary and self.primary_manufacturer_part is not None and self.primary_manufacturer_part.id is not None:
333
376
  return q.exclude(id=self.primary_manufacturer_part.id)
334
377
  return q
335
378
 
@@ -406,6 +449,68 @@ class Part(OrganizationScopedModel):
406
449
  return u'%s' % (self.full_part_number())
407
450
 
408
451
 
452
+ class QuantityOfMeasure(OrganizationOptionalModel):
453
+ """
454
+ Defines the physical dimension (e.g., Length, Voltage, Mass).
455
+ Acts as the 'bucket' for compatible units.
456
+ """
457
+ name = models.CharField(max_length=64, help_text="e.g. Voltage")
458
+
459
+ def get_base_unit(self):
460
+ return self.units.filter(base_multiplier=1.0).first()
461
+
462
+ class Meta:
463
+ unique_together = (('organization', 'name',),)
464
+
465
+ def __str__(self):
466
+ return self.name
467
+
468
+
469
+ class UnitDefinition(OrganizationOptionalModel):
470
+ """
471
+ Defines valid units.
472
+ """
473
+ name = models.CharField(max_length=64) # e.g. Millivolt
474
+ symbol = models.CharField(max_length=16) # e.g. mV
475
+ quantity_of_measure = models.ForeignKey(QuantityOfMeasure, on_delete=models.CASCADE, related_name='units')
476
+ base_multiplier = models.DecimalField(default=Decimal('1.0'), max_digits=40, decimal_places=20)
477
+
478
+ class Meta:
479
+ unique_together = (('organization', 'quantity_of_measure', 'symbol'),)
480
+ ordering = ['base_multiplier']
481
+
482
+ def __str__(self):
483
+ return f"{self.symbol}"
484
+
485
+
486
+ class PartRevisionPropertyDefinition(OrganizationOptionalModel):
487
+ code = models.CharField(max_length=64, blank=True) # The internal slug (e.g., max_operating_temp).
488
+ name = models.CharField(max_length=64) # The user-friendly text displayed in the UI
489
+ type = models.CharField(max_length=1, choices=PART_REVISION_PROPERTY_TYPES)
490
+ required = models.BooleanField(default=False)
491
+ quantity_of_measure = models.ForeignKey(QuantityOfMeasure, on_delete=models.SET_NULL, null=True, blank=True)
492
+
493
+ class Meta:
494
+ unique_together = ('organization', 'code',)
495
+
496
+ @property
497
+ def form_field_name(self):
498
+ return f'property_{self.code}'
499
+
500
+ @property
501
+ def form_unit_field_name(self):
502
+ return f'{self.form_field_name}_unit'
503
+
504
+ def __str__(self):
505
+ return self.name
506
+
507
+ def save(self, *args, **kwargs):
508
+ if not self.code:
509
+ self.code = slugify(self.name)
510
+
511
+ super(PartRevisionPropertyDefinition, self).save(*args, **kwargs)
512
+
513
+
409
514
  # Below are attributes of a part that can be changed, but it's important to trace the change over time
410
515
  class PartRevision(models.Model):
411
516
  part = models.ForeignKey(Part, on_delete=models.CASCADE, db_index=True)
@@ -415,55 +520,12 @@ class PartRevision(models.Model):
415
520
  assembly = models.ForeignKey('Assembly', default=None, null=True, on_delete=models.CASCADE, db_index=True)
416
521
  displayable_synopsis = models.CharField(editable=False, default="", null=True, blank=True, max_length=255, db_index=True)
417
522
  searchable_synopsis = models.CharField(editable=False, default="", null=True, blank=True, max_length=255, db_index=True)
523
+ description = models.CharField(max_length=255, default="", null=True, blank=True)
418
524
 
419
525
  class Meta:
420
526
  unique_together = (('part', 'revision'),)
421
527
  ordering = ['part']
422
528
 
423
- # Part Revision Specification Properties:
424
-
425
- description = models.CharField(max_length=255, default="", null=True, blank=True)
426
-
427
- # By convention for IndaBOM, for part revision properties below, if a property value has
428
- # an associated units of measure, and if the property value field name is 'vvv' then the
429
- # associated units of measure field name must be 'vvv_units'.
430
- value_units = models.CharField(max_length=5, default=None, null=True, blank=True, choices=VALUE_UNITS)
431
- value = models.CharField(max_length=255, default=None, null=True, blank=True)
432
- attribute = models.CharField(max_length=255, default=None, null=True, blank=True)
433
- pin_count = models.DecimalField(max_digits=3, decimal_places=0, default=None, null=True, blank=True)
434
- tolerance = models.CharField(max_length=6, validators=[validate_pct], default=None, null=True, blank=True)
435
- package = models.CharField(max_length=16, default=None, null=True, blank=True, choices=PACKAGE_TYPES)
436
- material = models.CharField(max_length=32, default=None, null=True, blank=True)
437
- finish = models.CharField(max_length=32, default=None, null=True, blank=True)
438
- color = models.CharField(max_length=32, default=None, null=True, blank=True)
439
- length_units = models.CharField(max_length=5, default=None, null=True, blank=True, choices=DISTANCE_UNITS)
440
- length = models.DecimalField(max_digits=7, decimal_places=3, default=None, null=True, blank=True)
441
- width_units = models.CharField(max_length=5, default=None, null=True, blank=True, choices=DISTANCE_UNITS)
442
- width = models.DecimalField(max_digits=7, decimal_places=3, default=None, null=True, blank=True)
443
- height_units = models.CharField(max_length=5, default=None, null=True, blank=True, choices=DISTANCE_UNITS)
444
- height = models.DecimalField(max_digits=7, decimal_places=3, default=None, null=True, blank=True)
445
- weight_units = models.CharField(max_length=5, default=None, null=True, blank=True, choices=WEIGHT_UNITS)
446
- weight = models.DecimalField(max_digits=7, decimal_places=3, default=None, null=True, blank=True)
447
- temperature_rating_units = models.CharField(max_length=5, default=None, null=True, blank=True, choices=TEMPERATURE_UNITS)
448
- temperature_rating = models.DecimalField(max_digits=7, decimal_places=3, default=None, null=True, blank=True)
449
- temperature_rating_range_max = models.DecimalField(max_digits=7, decimal_places=3, default=None, null=True, blank=True)
450
- temperature_rating_range_min = models.DecimalField(max_digits=7, decimal_places=3, default=None, null=True, blank=True)
451
- wavelength_units = models.CharField(max_length=5, default=None, null=True, blank=True, choices=WAVELENGTH_UNITS)
452
- wavelength = models.DecimalField(max_digits=7, decimal_places=3, default=None, null=True, blank=True)
453
- frequency_units = models.CharField(max_length=5, default=None, null=True, blank=True, choices=FREQUENCY_UNITS)
454
- frequency = models.DecimalField(max_digits=7, decimal_places=3, default=None, null=True, blank=True)
455
- memory_units = models.CharField(max_length=5, default=None, null=True, blank=True, choices=MEMORY_UNITS)
456
- memory = models.DecimalField(max_digits=7, decimal_places=3, default=None, null=True, blank=True)
457
- interface = models.CharField(max_length=12, default=None, null=True, blank=True, choices=INTERFACE_TYPES)
458
- power_rating_units = models.CharField(max_length=5, default=None, null=True, blank=True, choices=POWER_UNITS)
459
- power_rating = models.DecimalField(max_digits=7, decimal_places=3, default=None, null=True, blank=True)
460
- supply_voltage_units = models.CharField(max_length=5, default=None, null=True, blank=True, choices=VOLTAGE_UNITS)
461
- supply_voltage = models.DecimalField(max_digits=7, decimal_places=3, default=None, null=True, blank=True)
462
- voltage_rating_units = models.CharField(max_length=5, default=None, null=True, blank=True, choices=VOLTAGE_UNITS)
463
- voltage_rating = models.DecimalField(max_digits=7, decimal_places=3, default=None, null=True, blank=True)
464
- current_rating_units = models.CharField(max_length=5, default=None, null=True, blank=True, choices=CURRENT_UNITS)
465
- current_rating = models.DecimalField(max_digits=7, decimal_places=3, default=None, null=True, blank=True)
466
-
467
529
  def generate_synopsis(self, make_searchable=False):
468
530
  def verbosify(val, units=None, pre=None, pre_whitespace=True, post=None, post_whitespace=True):
469
531
  elaborated = ""
@@ -480,47 +542,52 @@ class PartRevision(models.Model):
480
542
  return elaborated
481
543
 
482
544
  s = ""
483
- s += verbosify(self.value, units=self.value_units if make_searchable else self.get_value_units_display())
484
545
  s += verbosify(self.description)
485
- tolerance = self.tolerance.replace('%', '') if self.tolerance else ''
486
- s += verbosify(tolerance, post='%', post_whitespace=False)
487
- s += verbosify(self.attribute)
488
- s += verbosify(self.package if make_searchable else self.get_package_display())
489
- s += verbosify(self.pin_count, post='pins')
490
- s += verbosify(self.frequency, units=self.frequency_units if make_searchable else self.get_frequency_units_display())
491
- s += verbosify(self.wavelength, units=self.wavelength_units if make_searchable else self.get_wavelength_units_display())
492
- s += verbosify(self.memory, units=self.memory_units if make_searchable else self.get_memory_units_display())
493
- s += verbosify(self.interface if make_searchable else self.get_interface_display())
494
- s += verbosify(self.supply_voltage, units=self.supply_voltage_units if make_searchable else self.get_supply_voltage_units_display(), post='supply')
495
- s += verbosify(self.temperature_rating, units=self.temperature_rating_units if make_searchable else self.get_temperature_rating_units_display(), post='rating')
496
- s += verbosify(self.power_rating, units=self.power_rating_units if make_searchable else self.get_power_rating_units_display(), post='rating')
497
- s += verbosify(self.voltage_rating, units=self.voltage_rating_units if make_searchable else self.get_voltage_rating_units_display(), post='rating')
498
- s += verbosify(self.current_rating, units=self.current_rating_units if make_searchable else self.get_current_rating_units_display(), post='rating')
499
- s += verbosify(self.material)
500
- s += verbosify(self.color)
501
- s += verbosify(self.finish)
502
- s += verbosify(self.length, units=self.length_units if make_searchable else self.get_length_units_display(), pre='L')
503
- s += verbosify(self.width, units=self.width_units if make_searchable else self.get_width_units_display(), pre='W')
504
- s += verbosify(self.height, units=self.height_units if make_searchable else self.get_height_units_display(), pre='H')
505
- s += verbosify(self.weight, units=self.weight_units if make_searchable else self.get_weight_units_display())
546
+
547
+ # TODO: We no longer order these, in the future we will add a template / inheritance type pattern like
548
+ # Capacitor: {Capacitance} {Units}, {Voltage}, {Package}
549
+ for prop in self.properties.all().select_related('property_definition', 'unit_definition'):
550
+ val = prop.value_raw
551
+ units = prop.unit_definition.symbol if prop.unit_definition else None
552
+ s += verbosify(val, units=units)
553
+
506
554
  return s[:255]
507
555
 
508
556
  def synopsis(self, return_displayable=True):
509
557
  return self.displayable_synopsis if return_displayable else self.searchable_synopsis
510
558
 
559
+ def update_synopsis(self):
560
+ self.searchable_synopsis = self.generate_synopsis(True)
561
+ self.displayable_synopsis = self.generate_synopsis(False)
562
+ # Use update() to avoid triggering save() again and potentially recursion
563
+ PartRevision.objects.filter(id=self.id).update(
564
+ searchable_synopsis=self.searchable_synopsis,
565
+ displayable_synopsis=self.displayable_synopsis
566
+ )
567
+
511
568
  def save(self, *args, **kwargs):
512
- if self.tolerance:
513
- self.tolerance = self.tolerance.replace('%', '')
514
569
  if self.assembly is None:
515
- assy = Assembly.objects.create()
516
- self.assembly = assy
517
- # if self.id:
518
- # previous_configuration = PartRevision.objects.get(id=self.id).configuration
519
- # if self.configuration != previous_configuration:
520
- # self.timestamp = timezone.now()
570
+ self.assembly = Assembly.objects.create()
571
+
572
+ super(PartRevision, self).save(*args, **kwargs)
573
+
521
574
  self.searchable_synopsis = self.generate_synopsis(True)
522
575
  self.displayable_synopsis = self.generate_synopsis(False)
523
- super(PartRevision, self).save(*args, **kwargs)
576
+
577
+ def get_field_value(self, field_name):
578
+ if hasattr(self, field_name):
579
+ return getattr(self, field_name)
580
+
581
+ is_unit = field_name.endswith('_units')
582
+ prop_name = field_name[:-6] if is_unit else field_name
583
+
584
+ for prop in self.properties.all():
585
+ if prop.property_definition.name == prop_name:
586
+ if is_unit:
587
+ return prop.unit_definition.symbol if prop.unit_definition else ''
588
+ else:
589
+ return prop.value_raw
590
+ return None
524
591
 
525
592
  def indented(self, top_level_quantity=100):
526
593
  def indented_given_bom(bom, part_revision, parent_id=None, parent=None, qty=1, parent_qty=1, indent_level=0, subpart=None, reference='', do_not_load=False):
@@ -644,6 +711,50 @@ class PartRevision(models.Model):
644
711
  return u'{}, Rev {}'.format(self.part.full_part_number(), self.revision)
645
712
 
646
713
 
714
+ class PartRevisionProperty(models.Model):
715
+ part_revision = models.ForeignKey(PartRevision, on_delete=models.CASCADE, related_name='properties')
716
+ property_definition = models.ForeignKey(PartRevisionPropertyDefinition, on_delete=models.CASCADE)
717
+ value_raw = models.CharField(max_length=255) # Base unit value, e.g. 0.01 (to describe 10mV)
718
+ unit_definition = models.ForeignKey(UnitDefinition, null=True, blank=True, on_delete=models.SET_NULL)
719
+ value_normalized = models.DecimalField(null=True, blank=True, max_digits=40, decimal_places=20)
720
+
721
+ def clean(self):
722
+ super().clean()
723
+
724
+ if self.unit_definition and self.property_definition.quantity_of_measure:
725
+ if self.unit_definition.quantity_of_measure != self.property_definition.quantity_of_measure:
726
+ raise ValidationError(
727
+ f"Unit '{self.unit_definition}' matches {self.unit_definition.quantity_of_measure}, but property requires {self.property_definition.quantity_of_measure}")
728
+
729
+ # Validate property is allowed for this part class
730
+ part = self.part_revision.part
731
+ if part.number_class:
732
+ allowed_definitions = part.number_class.property_definitions.all()
733
+ if not allowed_definitions.filter(id=self.property_definition.id).exists():
734
+ raise ValidationError(
735
+ f"The property '{self.property_definition.name}' is not valid for the Part Class '{part.number_class.name}'."
736
+ )
737
+
738
+ def save(self, *args, **kwargs):
739
+ if self.unit_definition and self.value_raw:
740
+ try:
741
+ val = Decimal(str(self.value_raw))
742
+ multiplier = Decimal(str(self.unit_definition.base_multiplier))
743
+ self.value_normalized = val * multiplier
744
+ except (ValueError, InvalidOperation):
745
+ self.value_normalized = None
746
+ else:
747
+ self.value_normalized = None
748
+ super(PartRevisionProperty, self).save(*args, **kwargs)
749
+
750
+ if self.part_revision:
751
+ self.part_revision.update_synopsis()
752
+
753
+ def __str__(self):
754
+ unit_sym = self.unit_definition.symbol if self.unit_definition else ""
755
+ return f"{self.property_definition.name}: {self.value_raw} {unit_sym}"
756
+
757
+
647
758
  class AssemblySubparts(models.Model):
648
759
  assembly = models.ForeignKey('Assembly', models.CASCADE)
649
760
  subpart = models.ForeignKey('Subpart', models.CASCADE)
@@ -89,6 +89,25 @@ td, tr {
89
89
  white-space: nowrap;
90
90
  }
91
91
 
92
+ .table-action {
93
+ display: inline-flex;
94
+ align-items: center;
95
+ vertical-align: middle;
96
+ font-size: 0.9rem;
97
+ font-weight: 500;
98
+ }
99
+
100
+ /* 2. Tighten icons so they don't stretch the row height */
101
+ .table-action .material-icons {
102
+ font-size: 1.1rem !important;
103
+ margin-right: 4px;
104
+ }
105
+
106
+ /* 3. Give consecutive actions a little breathing room */
107
+ .table-action + .table-action {
108
+ margin-left: 12px;
109
+ }
110
+
92
111
  .responsive-table-wrapper {
93
112
  overflow-x: auto;
94
113
  padding-bottom: 16px;
@@ -0,0 +1,65 @@
1
+ function initFormset(options) {
2
+ const {
3
+ prefix,
4
+ addBtnId,
5
+ formContainerId,
6
+ emptyFormTemplateId,
7
+ rowSelector,
8
+ onRowAdd
9
+ } = options;
10
+
11
+ const totalForms = document.getElementById(`id_${prefix}-TOTAL_FORMS`);
12
+ const formContainer = document.getElementById(formContainerId);
13
+ const emptyFormTemplate = document.getElementById(emptyFormTemplateId).innerHTML;
14
+ const addBtn = document.getElementById(addBtnId);
15
+
16
+ if (addBtn) {
17
+ addBtn.addEventListener('click', function (e) {
18
+ if (e) e.preventDefault();
19
+ const currentFormCount = parseInt(totalForms.value);
20
+ const newRowHtml = emptyFormTemplate.replace(/__prefix__/g, currentFormCount);
21
+
22
+ formContainer.insertAdjacentHTML('beforeend', newRowHtml);
23
+ totalForms.value = currentFormCount + 1;
24
+
25
+ const newRow = formContainer.lastElementChild;
26
+
27
+ // Re-initialize Materialize components
28
+ if (typeof M !== 'undefined') {
29
+ const selects = newRow.querySelectorAll('select');
30
+ if (selects.length > 0) {
31
+ M.FormSelect.init(selects);
32
+ }
33
+ M.updateTextFields();
34
+ }
35
+
36
+ if (onRowAdd) {
37
+ onRowAdd(newRow);
38
+ }
39
+ });
40
+ }
41
+
42
+ formContainer.addEventListener('click', function (e) {
43
+ // REMOVE NEW ROW (Client-side only)
44
+ const removeBtn = e.target.closest('.remove-new-row');
45
+ if (removeBtn) {
46
+ e.preventDefault();
47
+ const row = removeBtn.closest(rowSelector);
48
+ row.remove();
49
+ // We don't necessarily decrement totalForms.value here as Django handles missing indexes,
50
+ // but some implementations do. Given the original code didn't, we won't either unless needed.
51
+ }
52
+
53
+ // DELETE EXISTING ROW (Server-side logic via DELETE checkbox)
54
+ const deleteBtn = e.target.closest('.delete-existing-row');
55
+ if (deleteBtn) {
56
+ e.preventDefault();
57
+ const row = deleteBtn.closest(rowSelector);
58
+ const checkbox = row.querySelector('input[type="checkbox"][name$="-DELETE"]');
59
+ if (checkbox) {
60
+ checkbox.checked = true;
61
+ row.style.display = 'none';
62
+ }
63
+ }
64
+ });
65
+ }
@@ -1,26 +1,89 @@
1
1
  {% extends 'bom/bom-base.html' %}
2
-
3
2
  {% load static %}
4
3
  {% load materializecss %}
5
4
 
6
5
  {% block head-title %}{{ title }}{% endblock %}
7
6
 
8
7
  {% block content %}
9
- <div class="container-app">
8
+ <div class="container" style="padding-top: 40px; max-width: 1000px;">
10
9
  {% if profile.role == 'A' %}
11
10
  <div class="row">
12
- <form action="{% url 'bom:part-class-edit' part_class_id=part_class.id %}" method="post" class="col s12">
11
+ <form action="{% url 'bom:part-class-edit' part_class_id=part_class.id %}" method="post"
12
+ class="col s12">
13
13
  {% csrf_token %}
14
- {{ part_class_form.non_field_errors }}
15
- {{ part_class_form.code|materializecss:'s12 m2' }}
16
- {{ part_class_form.name|materializecss:'s12 m6' }}
17
- {{ part_class_form.comment|materializecss:'s12 m12' }}
14
+
18
15
  <div class="row">
19
- <div class="col s6">
20
- <a href="{% url 'bom:settings' %}indabom" class="waves-effect waves-light btn-flat grey-text lighten-1" style="margin-left: -16px;">Cancel</a>
16
+ <div class="col s12 m3">
17
+ {{ part_class_form.code|materializecss }}
18
+ </div>
19
+ <div class="col s12 m9">
20
+ {{ part_class_form.name|materializecss }}
21
+ </div>
22
+ <div class="col s12">
23
+ {{ part_class_form.comment|materializecss }}
24
+ </div>
25
+ </div>
26
+
27
+ <div class="row" style="margin-top: 40px; margin-bottom: 20px;">
28
+ <div class="col s12">
29
+ <h5>Property Definitions</h5>
30
+ <p class="grey-text text-darken-1">Define the custom fields available to parts in this
31
+ class.</p>
32
+ </div>
33
+ </div>
34
+
35
+ <div id="property-definitions-wrapper">
36
+ {{ property_definitions_formset.management_form }}
37
+
38
+ <table class="highlight" style="border-bottom: 1px solid #e0e0e0;">
39
+ <thead>
40
+ <tr class="grey-text">
41
+ <th style="width: 90%;">Property Definition</th>
42
+ <th style="width: 10%;" class="center-align">Action</th>
43
+ </tr>
44
+ </thead>
45
+ <tbody id="property-definitions-body">
46
+ {% for form in property_definitions_formset %}
47
+ <tr class="property-definition-row">
48
+ {% for hidden in form.hidden_fields %}
49
+ {{ hidden }}
50
+ {% endfor %}
51
+
52
+ <td>{{ form.property_definition|materializecss }}</td>
53
+ <td class="center-align">
54
+ {% if forloop.counter0 < property_definitions_formset.initial_form_count %}
55
+ <div style="display:none;">{{ form.DELETE }}</div>
56
+ <a href="#!" class="btn-flat btn-small red-text delete-existing-row">
57
+ <i class="material-icons">delete</i>
58
+ </a>
59
+ {% else %}
60
+ <a href="#!" class="btn-flat btn-small red-text remove-new-row">
61
+ <i class="material-icons">close</i>
62
+ </a>
63
+ {% endif %}
64
+ </td>
65
+ </tr>
66
+ {% endfor %}
67
+ </tbody>
68
+ </table>
69
+
70
+ <div class="row" style="margin-top: 15px;">
71
+ <div class="col s12">
72
+ <button type="button" class="btn-flat waves-effect"
73
+ id="add-property-definition" style="padding-left: 0;">
74
+ <i class="material-icons left">add</i> Add Property
75
+ </button>
76
+ </div>
21
77
  </div>
22
- <div class="col s6 right-align">
23
- <button class="waves-effect waves-light btn btn-primary" type="submit" name="action">Save
78
+ </div>
79
+
80
+ <div class="row" style="margin-top: 50px;">
81
+ <div class="col s12 right-align">
82
+ <a href="{% url 'bom:settings' %}indabom"
83
+ class="btn btn-flat waves-effect"
84
+ style="margin-right: 10px;">Cancel</a>
85
+ <button class="waves-effect waves-light btn btn-primary" type="submit" name="action">
86
+ Save
24
87
  </button>
25
88
  </div>
26
89
  </div>
@@ -30,4 +93,28 @@
30
93
  {% include 'bom/nothing-to-see.html' with required_privilege='Admin' %}
31
94
  {% endif %}
32
95
  </div>
96
+
97
+ <script type="text/template" id="empty-form-template">
98
+ <tr class="property-definition-row">
99
+ <td>{{ property_definitions_formset.empty_form.property_definition|materializecss }}</td>
100
+ <td class="center-align">
101
+ <a href="#!" class="btn-flat btn-small red-text remove-new-row">
102
+ <i class="material-icons">close</i>
103
+ </a>
104
+ </td>
105
+ </tr>
106
+ </script>
107
+
108
+ <script src="{% static 'bom/js/formset-handler.js' %}"></script>
109
+ <script>
110
+ document.addEventListener('DOMContentLoaded', function () {
111
+ initFormset({
112
+ prefix: '{{ property_definitions_formset.prefix }}',
113
+ addBtnId: 'add-property-definition',
114
+ formContainerId: 'property-definitions-body',
115
+ emptyFormTemplateId: 'empty-form-template',
116
+ rowSelector: '.property-definition-row'
117
+ });
118
+ });
119
+ </script>
33
120
  {% endblock %}