CONFIGURATION_TYPES = ((CONFIGURATION_TYPE_RELEASED, 'Released'), (CONFIGURATION_TYPE_WORKING, 'Working'),) CURRENT_UNITS = (
    NO_CHOICE,
    ('A', 'A'),
    ('uA', '\u03BCV'),
    ('mA', 'mA'),
    ('kA', 'kA'),
    ('MA', 'MA'),
    ('Other', 'Other'),
) DISTANCE_UNITS = (
    NO_CHOICE,
    ('mil', 'mil'),
    ('in', 'in'),
    ('ft', 'ft'),
    ('yd', 'yd'),
    ('km', 'km'),
    ('m', 'm'),
    ('cm', 'cm'),
    ('mm', 'mm'),
    ('um', '\u03BCm'),
    ('nm', 'nm'),
    ('Other', 'Other'),
) FREQUENCY_UNITS = (
    NO_CHOICE,
    ('Hz', 'Hz'),
    ('kHz', 'kHz'),
    ('MHz', 'MHz'),
    ('GHz', 'GHz'),
    ('Other', 'Other'),
) INTERFACE_TYPES = (
    NO_CHOICE,
    ('I2C', 'I2C'),
    ('SPI', 'SPI'),
    ('CAN', 'CAN'),
    ('One-Wire', '1-Wire'),
    ('RS485', 'RS-485'),
    ('RS232', 'RS-232'),
    ('WiFi', 'Wi-Fi'),
    ('4G', '4G'),
    ('BT', 'Bluetooth'),
    ('BTLE', 'Bluetooth LE'),
    ('Z_Wave', 'Z-Wave'),
    ('Zigbee', 'Zigbee'),
    ('LAN', 'LAN'),
    ('USB', 'USB'),
    ('HDMI', 'HDMI'),
    ('Other', 'Other'),
) MEMORY_UNITS = (
    NO_CHOICE,
    ('KB', 'KB'),
    ('MB', 'MB'),
    ('GB', 'GB'),
    ('TB', 'TB'),
    ('Other', 'Other'),
) NUMBER_CLASS_CODE_LEN_DEFAULT = 3 NUMBER_CLASS_CODE_LEN_MAX = 16 NUMBER_CLASS_CODE_LEN_MIN = 2 NUMBER_ITEM_LEN_DEFAULT = 4 NUMBER_ITEM_LEN_MAX = 128 NUMBER_ITEM_LEN_MIN = 3 NUMBER_SCHEME_INTELLIGENT = 'I' NUMBER_SCHEME_SEMI_INTELLIGENT = 'S' NUMBER_VARIATION_LEN_DEFAULT = 2 NUMBER_VARIATION_LEN_MAX = 16 NUMBER_VARIATION_LEN_MIN = 0 PACKAGE_TYPES = (
    NO_CHOICE,
    ('0201 smd', '0201 smd'),
    ('0402 smd', '0402 smd'),
    ('0603 smd', '0603 smd'),
    ('0805 smd', '0805 smd'),
    ('1206 smd', '1206 smd'),
    ('1210 smd', '1210 smd'),
    ('1812 smd', '1812 smd'),
    ('2010 smd', '2010 smd'),
    ('2512 smd', '2512 smd'),
    ('1/8 radial', '1/8 radial'),
    ('1/4 radial', '1/4 radial'),
    ('1/2 radial', '1/2 radial'),
    ('Size A', 'Size A'),
    ('Size B', 'Size B'),
    ('Size C', 'Size C'),
    ('Size D', 'Size D'),
    ('Size E', 'Size E'),
    ('SOT-23', 'SOT-23'),
    ('SOT-223', 'SOT-233'),
    ('DIL', 'DIL'),
    ('SOP', 'SOP'),
    ('SOIC', 'SOIC'),
    ('QFN', 'QFN'),
    ('QFP', 'QFP'),
    ('QFT', 'QFT'),
    ('PLCC', 'PLCC'),
    ('VGA', 'VGA'),
    ('Other', 'Other'),
) POWER_UNITS = (
    NO_CHOICE,
    ('W', 'W'),
    ('uW', '\u03BCW'),
    ('mW', 'mW'),
    ('kW', 'kW'),
    ('MW', 'MW'),
    ('Other', 'Other'),
) ROLE_TYPE_VIEWER = 'V' ROLE_TYPES = ((ROLE_TYPE_ADMIN, 'Admin'), (ROLE_TYPE_VIEWER, 'Viewer'),) SUBSCRIPTION_TYPES = ((SUBSCRIPTION_TYPE_FREE, 'Free'), (SUBSCRIPTION_TYPE_PRO, 'Pro'),) TEMPERATURE_UNITS = (
    NO_CHOICE,
    ('C', '\u00B0C'),
    ('F', '\u00B0F'),
    ('Other', 'Other'),
) VALUE_UNITS = (
    NO_CHOICE,
    ('Ohms', '\u03A9'),
    ('mOhms', 'm\u03A9'),
    ('kOhms', 'k\u03A9'),
    ('MOhms', 'M\u03A9'),
    ('F', 'F'),
    ('pF', 'pF'),
    ('nF', 'nF'),
    ('uF', '\u03BCF'),
    ('V', 'V'),
    ('uV', '\u03BCV'),
    ('mV', 'mV'),
    ('A', 'A'),
    ('uA', '\u03BCA'),
    ('mA', 'mA'),
    ('C', '\u00B0C'),
    ('F', '\u00B0F'),
    ('H', 'H'),
    ('nH', 'nH'),
    ('mH', 'mH'),
    ('uH', '\u03BCH'),
    ('Hz', 'Hz'),
    ('kHz', 'kHz'),
    ('MHz', 'MHz'),
    ('GHz', 'GHz'),
    ('Other', 'Other'),
) VOLTAGE_UNITS = (
    NO_CHOICE,
    ('V', 'V'),
    ('uV', '\u03BCV'),
    ('mV', 'mV'),
    ('kV', 'kV'),
    ('MV', 'MV'),
    ('Other', 'Other'),
) WAVELENGTH_UNITS = (
    NO_CHOICE,
    ('km', 'km'),
    ('m', 'm'),
    ('cm', 'cm'),
    ('mm', 'mm'),
    ('um', '\u03BCm'),
    ('nm', 'nm'),
    ('A', '\u212B'),
    ('Other', 'Other'),
) WEIGHT_UNITS = (
    NO_CHOICE,
    ('mg', 'mg'),
    ('g', 'g'),
    ('kg', 'kg'),
    ('oz', 'oz'),
    ('lb', 'lb'),
    ('Other', 'Other'),
) class BOMFlatCSVHeaders(CSVHeaders):
    all_headers_defns = [
        CSVHeader('part_number', name_options=['part number', 'part no', ]),
        CSVHeader('quantity', name_options=['count', 'qty', ]),
        CSVHeader('do_not_load', name_options=['dnl', 'dnp', 'do_not_populate', 'do_not_load', 'do not load', 'do not populate', ]),
        CSVHeader('part_class', name_options=['class', 'part_category']),
        CSVHeader('references', name_options=['designator', 'designators', 'reference', ]),
        CSVHeader('synopsis', name_options=['part_synopsis', ]),
        CSVHeader('revision', name_options=['rev', 'part_revision', 'rev.']),
    ] + ManufacturerPartCSVHeaders.all_headers_defns \
      + SellerPartCSVHeaders.all_headers_defns + [
        CSVHeader('extended_qty', name_options=['extended_quantity', 'part_extended_quantity', 'part_ext_qty', ]),
        CSVHeader('extended_cost', name_options=['part_extended_cost', 'part_ext_cost', ]),
        CSVHeader('order_qty', name_options=['part_order_qty', 'part_order_quantity', 'order_quantity', ]),
        CSVHeader('out_of_pocket_cost', name_options=['part_out_of_pocket_cost', 'cost', ]),
        CSVHeader('lead_time_days', name_options=['part_lead_time_days', ]),
    ] class BOMIndentedCSVHeaders(BOMFlatCSVHeaders):
    all_headers_defns = [CSVHeader('level')] + BOMFlatCSVHeaders.all_headers_defns class CSVHeaderError(Exception):
    def __init__(self, str):
        self.str = str

    def __str__(self):
        return self.str class PartClassesCSVHeaders(CSVHeaders):
    all_headers_defns = [
        CSVHeader('code'),
        CSVHeader('name'),
        CSVHeader('comment', name_options=['description', 'desc', 'desc.']),
        CSVHeader('mouser_enabled'),
    ] class PartsListCSVHeaders(CSVHeaders):
    part_attributes = [
        CSVHeader('value', name_options=['val', 'val.', ]),
        CSVHeader('value_units', name_options=['value units', 'val. units', 'val units', ]),
        CSVHeader('tolerance', name_options=[]),
        CSVHeader('attribute', name_options=[]),
        CSVHeader('package', name_options=[]),
        CSVHeader('pin_count', name_options=[]),
        CSVHeader('frequency', name_options=[]),
        CSVHeader('frequency_units', name_options=[]),
        CSVHeader('wavelength', name_options=[]),
        CSVHeader('wavelength_units', name_options=[]),
        CSVHeader('memory', name_options=[]),
        CSVHeader('memory_units', name_options=[]),
        CSVHeader('interface', name_options=[]),
        CSVHeader('supply_voltage', name_options=[]),
        CSVHeader('supply_voltage_units', name_options=[]),
        CSVHeader('temperature_rating', name_options=[]),
        CSVHeader('temperature_rating_units', name_options=[]),
        CSVHeader('power_rating', name_options=[]),
        CSVHeader('power_rating_units', name_options=[]),
        CSVHeader('voltage_rating', name_options=[]),
        CSVHeader('voltage_rating_units', name_options=[]),
        CSVHeader('current_rating', name_options=[]),
        CSVHeader('current_rating_units', name_options=[]),
        CSVHeader('material', name_options=[]),
        CSVHeader('color', name_options=[]),
        CSVHeader('finish', name_options=[]),
        CSVHeader('length', name_options=[]),
        CSVHeader('length_units', name_options=[]),
        CSVHeader('width', name_options=[]),
        CSVHeader('width_units', name_options=[]),
        CSVHeader('height', name_options=[]),
        CSVHeader('height_units', name_options=[]),
        CSVHeader('weight', name_options=[]),
        CSVHeader('weight_units', name_options=[]),
    ]

    all_headers_defns = [
        CSVHeader('description', name_options=['desc', 'desc.', ]),
        CSVHeader('manufacturer_name', name_options=['mfg_name', 'manufacturer_name', 'part_manufacturer', 'mfg', 'manufacturer', 'manufacturer name', ]),
        CSVHeader('manufacturer_part_number', name_options=['mpn', 'mfg_part_number', 'part_manufacturer_part_number', 'mfg part number', 'manufacturer part number']),
        CSVHeader('part_number', name_options=['part number', 'part no', ]),
        CSVHeader('revision', name_options=['rev', 'part_revision', ]),
    ] + part_attributes \
      + SellerPartCSVHeaders.all_headers_defns class AutocompleteTextInput(forms.TextInput):
    def __init__(self, *args, **kwargs):
        self.queryset = kwargs.pop('queryset')
        self.autocomplete_limit = kwargs.pop('autocomplete_limit', None)
        self.autocomplete_min_length = kwargs.pop('autocomplete_min_length', 0)
        self.autocomplete_submit = kwargs.pop('autocomplete_submit', False)
        self.form_name = kwargs.pop('form_name', 'form')
        self.verbose_string_function = kwargs.pop('verbose_string_function', str)
        super().__init__(*args, **kwargs)

    def render(self, name, value, attrs=None, renderer=None):
        # verbose_string_function is used if we want to show verbose strings in the dropdown,
        # but autocomplete to something simpler

        # Disable chrome autocomplete..we dont want double duty here!
        if attrs is not None:
            attrs.update({'autocomplete': 'off'})
        else:
            attrs['autocomplete'] = "off"

        html = super().render(name, value, attrs)

        autocomplete_dict = {}
        autocomplete_dict_to_fill = {}
        for obj in self.queryset:
            verbose_obj_str = self.verbose_string_function(obj).replace('"', '\\"')
            autocomplete_dict.update({verbose_obj_str: None})
            autocomplete_dict_to_fill.update({verbose_obj_str: str(obj)})

        autocomplete_json = dumps(autocomplete_dict).replace("'", "\\'")
        autocomplete_json_to_fill = dumps(autocomplete_dict_to_fill).replace("'", "\\'")

        # To escape brackets in a Python 3.6 f-string we use double brackets
        inline_code = mark_safe(
            f"""<script>
            const {name}_data = JSON.parse('{autocomplete_json}');
            const {name}_data_to_fill = JSON.parse('{autocomplete_json_to_fill}');
            const {name}_input = document.getElementById("id_{name}");
            const {name}_form = {name}_input.form;
            $(document).ready(function () {{
                $('#id_{name}').autocomplete({{
                    data: {name}_data,
                    limit: {self.autocomplete_limit or 'undefined'}, // The max amount of results that can be shown at once. Default: Infinity.
                    minLength: {self.autocomplete_min_length}, // The minimum length of the input for the autocomplete to start. Default: 1.
                    onAutocomplete: function (val) {{
                        console.log({name}_data_to_fill);
                        $("#id_{name}").val({name}_data_to_fill[val]);
                        {f'{name}_form.submit()' if self.autocomplete_submit else ''}
                    }},
                }});
            }});
            </script>"""
        )
        return html + inline_code class Organization(models.Model):
class UserMeta(models.Model):
class PartClass(models.Model):
    class Meta:
class Manufacturer(models.Model, AsDictModel):
    class Meta:
class Part(models.Model):
    class Meta:
class PartRevision(models.Model):
    class Meta:
class AssemblySubparts(models.Model):
    class Meta:
class Subpart(models.Model):
class Assembly(models.Model):
class ManufacturerPart(models.Model, AsDictModel):
    class Meta:
class Seller(models.Model, AsDictModel):
class SellerPart(models.Model, AsDictModel):
    class Meta():
    def number_cs(self):
    def number_ns(self):
    def number_vs(self):
    def __str__(self):
    def seller_parts(self):
    def part_list_csv_headers(self):
    def email(self):
    def save(self, *args, **kwargs):
    def get_or_create_organization(self):
    def google_authenticated(self) -> bool:
    def is_organization_owner(self) -> bool:
    def _user_meta(self, organization=None):
    def __str__(self):
    def __str__(self):
    def full_part_number(self):
    def verify_format_number_class(number_class, organization):
    def verify_format_number_item(number_item, organization):
    def verify_format_number_variation(number_variation, organization):
    def parse_part_number(part_number, organization):
    def parse_partial_part_number(part_number, organization, validate=True):
    def description(self):
    def latest(self):
    def revisions(self):
    def seller_parts(self, exclude_primary=False):
    def manufacturer_parts(self, exclude_primary=False):
    def where_used(self):
    def where_used_full(self):
        def where_used_given_part(used_in_parts, part):
    def indented(self, part_revision=None):
    def optimal_seller(self, quantity=None):
    def assign_part_number(self):
    def save(self, *args, **kwargs):
    def verbose_str(self):
    def __str__(self):
    def generate_synopsis(self, make_searchable=False):
        def verbosify(val, units=None, pre=None, pre_whitespace=True, post=None, post_whitespace=True):
    def synopsis(self, return_displayable=True):
    def save(self, *args, **kwargs):
    def indented(self, top_level_quantity=100):
        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):
    def flat(self, top_level_quantity=100, sort=False):
        def flat_given_bom(bom, part_revision, parent=None, qty=1, parent_qty=1, subpart=None, reference=''):
        def sort_by_references(p):
    def where_used(self):
    def where_used_full(self):
        def where_used_given_part(used_in_parts, part):
    def next_revision(self):
    def __str__(self):
    def save(self, *args, **kwargs):
    def __str__(self):
    def seller_parts(self):
    def optimal_seller(self, quantity=None):
    def as_dict_for_export(self):
    def __str__(self):
    def __str__(self):
    def as_dict(self):
    def as_dict_for_export(self):
    def optimal(sellerparts, quantity):
    def order_quantity(self, extended_quantity):
    def __str__(self):
                FORMATS = {
                    1: '{0:0=1d}', 2: '{0:0=2d}', 3: '{0:0=3d}', 4: '{0:0=4d}', 5: '{0:0=5d}',
                    6: '{0:0=6d}', 7: '{0:0=7d}', 8: '{0:0=8d}', 9: '{0:0=9d}', 10: '{0:0=10d}'
                } def check_references_for_duplicates(new_refs, seen_refs, duplicate_refs):
    new_refs_list = listify_string(new_refs)
    new_refs_set = set()
    for r in new_refs_list:
        if r in new_refs_set:
            duplicate_refs.add(r)
        else:
            new_refs_set.add(r)
            if r in seen_refs:
                duplicate_refs.add(r)
        seen_refs.add(r) def get_from_dict(input_dict, key_options):
    for key in key_options:
        val = input_dict.get(key, None)
        if val:
            return val
    return None def listify_string(st):
    if st is None:
        return []
    ss = re.split(' |:|;|,|\t|\n', st)
    split_st = []
    for s in ss:
        s_strip = s.strip()
        if len(s_strip) != 0:
            split_st.append(s_strip)
    return split_st def prep_for_sorting_nicely(item):
    convert = lambda text: int(text) if text.isdigit() else text
    alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
    return alphanum_key(item) def stringify_list(li):
    return ', '.join(str(x) for x in li) def validate_pct(value):
import codecs
import csv
import logging
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.core.exceptions import ValidationError
from django.core.validators import MaxLengthValidator, MaxValueValidator, MinLengthValidator, MinValueValidator
from django.db import IntegrityError
from django.forms.models import model_to_dict
from django.utils.translation import gettext_lazy as _
from djmoney.money import Money
from .constants import (
    CONFIGURATION_TYPES,
    CURRENT_UNITS,
    DISTANCE_UNITS,
    FREQUENCY_UNITS,
    INTERFACE_TYPES,
    MEMORY_UNITS,
    NUMBER_CLASS_CODE_LEN_DEFAULT,
    NUMBER_CLASS_CODE_LEN_MAX,
    NUMBER_CLASS_CODE_LEN_MIN,
    NUMBER_ITEM_LEN_DEFAULT,
    NUMBER_ITEM_LEN_MAX,
    NUMBER_ITEM_LEN_MIN,
    NUMBER_SCHEME_INTELLIGENT,
    NUMBER_SCHEME_SEMI_INTELLIGENT,
    NUMBER_VARIATION_LEN_DEFAULT,
    NUMBER_VARIATION_LEN_MAX,
    NUMBER_VARIATION_LEN_MIN,
    PACKAGE_TYPES,
    POWER_UNITS,
    ROLE_TYPE_VIEWER,
    ROLE_TYPES,
    SUBSCRIPTION_TYPES,
    TEMPERATURE_UNITS,
    VALUE_UNITS,
    VOLTAGE_UNITS,
    WAVELENGTH_UNITS,
    WEIGHT_UNITS,
)
from .csv_headers import (
    BOMFlatCSVHeaders,
    BOMIndentedCSVHeaders,
    CSVHeaderError,
    PartClassesCSVHeaders,
    PartsListCSVHeaders,
)
from .form_fields import AutocompleteTextInput
from .models import (
    Assembly,
    AssemblySubparts,
    Manufacturer,
    ManufacturerPart,
    Organization,
    Part,
    PartClass,
    PartRevision,
    Seller,
    SellerPart,
    Subpart,
    User,
    UserMeta,
)
from .utils import (
    check_references_for_duplicates,
    get_from_dict,
    listify_string,
    prep_for_sorting_nicely,
    stringify_list,
)
from .validators import alphanumeric, decimal, numeric
                            continue
                    else:
                        subparts = Part.objects.filter(number_class=None, number_item=part_number, number_variation=None, organization=self.organization)

                    if len(subparts) == 0:
                        self.add_error(None,
                                       "Part {0} for subpart on row {1} does not exist, you must create the part "
                                       "before it can be added as a subpart. "
                                       "Uploading of this subpart skipped.".format(part_number, row_count))
                        continue
                    elif len(subparts) > 1:
                        self.add_error(None,
                                       "Found {0} entries for part {1} for subpart on row {2}. This should not happen. "
                                       "Please let info@indabom.com know. Uploading of this subpart skipped.".format(
                                           len(subparts), row_count, part_number))
                        continue

                    subpart_part = subparts[0]  # TODO: handle more than one subpart
                # Couldn't add the sub-part based on the part number, so try from MPN
                elif mpn:
                    manufacturer_parts = ManufacturerPart.objects.filter(manufacturer_part_number=mpn, part__organization=self.organization)
                    if len(manufacturer_parts) == 0:
                        self.add_error(None, "Manufacturer part number {0} for subpart on row {1} does not exist, "
                                             "you must create the part before it can be added as a subpart. "
                                             "Uploading of this part skipped.".format(part_dict['manufacturer_part_number'], row_count))
                        continue

                    subpart_part = manufacturer_parts[0].part
                else:
                    self.add_error(None, f"Couldn't parse part on row {row_count} with data: {row}.")
                    continue

                # We have the subpart_part at this point, make sure we don't have infinite recursion
                if self.parent_part == subpart_part:
                    # Silently skip any rows that contain the parent part
                    continue

                subpart_revision = subpart_part.latest()
                if not subpart_revision:
                    self.add_error(None, f"Part {part_number} has no revisions. Cannot add it to a BOM. Uploading of this subpart skipped.")
                    continue

                if revision:
                    revs = subpart_part.revisions().filter(revision=revision).order_by('-timestamp')
                    if revs.count() > 0:
                        subpart_revision = revs[0]
                    else:
                        rev_options = revs.values_list('revision', flat=True)
                        self.add_error(None, f"Found part {part_number}, but could not match revision {revision}. Options are: {rev_options}. Uploading of this subpart skipped.")
                        continue

                contains_parent = False
                indented_bom = subpart_revision.indented()
                for _, sp in indented_bom.parts.items():  # Make sure the subpart does not contain the parent - infinite recursion!
                    if sp.part_revision == parent_part_revision:
                        contains_parent = True
                if contains_parent:
                    self.add_error(None, f"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.")
                    continue

                # Decide if the current subpart (i.e., for current row) should:
                # a. Be combined with a previously seen subpart
                # b. Rejected because it is a duplicate of a previously seen subpart
                # c. Be imported as a new subpart
                #
                # Do (a) if current subpart's part matches a previous subpart's part except for their designators. Combine if
                # current subpart's designators either don't overlap or both subparts do not have designators. As
                # part of the combing, adjust subpart count to represent the total number of combined subparts. Reject
                # current subpart if it has a designator but the previous subpart does not (or vice versa).
                #
                # Do (b) if current subpart's part matches a previous subpart's part - AND - their designators match (this
                # includes the case where the current and previous subpart do not have any designators.
                #
                # Do (c) if the current subpart's part does not match any previous subpart's part - OR - if there is a
                # match except for their DNL flags. Want to have separate entries in the BOM for the to-be-loaded and
                # the do-not-load versions of these subparts.
                #

                existing_subpart_qs = parent_part_revision.assembly.subparts.filter(
                    part_revision=subpart_revision,
                    do_not_load=do_not_load # Include DNL flag in filter per (c) above
                )

                if len(existing_subpart_qs) == 0:

                    if len(reference_list) != count and len(reference_list) > 0:
                        self.warnings.append(
                            f"The number of reference designators ({len(reference_list)}) for subpart {subpart_part} on row {row_count} does not match the subpart quantity ({count}). Quantity automatically adjusted.")
                        count = len(reference_list)

                    new_subpart = Subpart.objects.create(
                        part_revision=subpart_revision,
                        count=count,
                        reference=reference,
                        do_not_load=do_not_load
                    )

                    AssemblySubparts.objects.get_or_create(assembly=parent_part_revision.assembly, subpart=new_subpart)

                    info_msg = f"Added subpart {part_number} on row {row_count} "
                    if reference:
                        info_msg += f"with reference designators {reference} "
                    info_msg += f"to parent part {self.parent_part}."
                    self.successes.append(info_msg)

                else:
                    existing_subpart = existing_subpart_qs[0]
                    existing_subpart_reference_list = listify_string(existing_subpart.reference)
                    if len(reference_list) == 0 or len(existing_subpart_reference_list) == 0:
                        self.add_error(None,
                            f"Cannot combine subpart {part_number} on row {row_count} with existing matching subpart because one or both subparts have empty reference designators. Subpart combining skipped.")
                    else:
                        combine = True
                        for ref in reference_list:
                            if ref in existing_subpart_reference_list:
                                combine = False
                                self.warnings.append(
                                    f"Cannot combine subpart subpart {part_number} on row {row_count} with existing subpart because one or more reference designators are the same. Subpart combining skipped.")
                                break
                        if combine:

                            info_msg = f"Combined reference designators {stringify_list(reference_list)} for subpart on row {row_count} with existing subpart {part_number}."