From 76eff4a57b49a075d4d220327e65a3438e720385 Mon Sep 17 00:00:00 2001 From: T0jan Date: Wed, 31 Jan 2024 17:28:44 +0100 Subject: [PATCH 1/6] add support for hierarchical parameter maps --- README.md | 2 + kintree/config/config_interface.py | 42 ++++++++---- .../config/inventree/supplier_parameters.yaml | 65 +++++++++---------- 3 files changed, 60 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index feee5e70..ee86ba23 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,8 @@ CATEGORY_NAME: - SUPPLIER_2_PARAMETER_NAME_1 ``` +It is also possible to cross reference the mappings of different categories. To define one or multiple parent categories a parameter named `parent` can be added where the items then are the desired parent categories. If a parameter name is present in both parent and child, the childs definition will override the parent. + Refer to [this file](https://github.com/sparkmicro/Ki-nTree/blob/main/kintree/config/inventree/supplier_parameters.yaml) as a starting point / example. #### Part Number Search diff --git a/kintree/config/config_interface.py b/kintree/config/config_interface.py index 6d6c0849..e640c53d 100644 --- a/kintree/config/config_interface.py +++ b/kintree/config/config_interface.py @@ -430,25 +430,39 @@ def add_supplier_category(categories: dict, supplier_config_path: str) -> bool: def load_category_parameters(categories: list, supplier_config_path: str) -> dict: ''' Load Supplier parameters mapping from Supplier settings file ''' + def find_parameters(output_dict, category_list): + category_parameters = None + combined = '' + for category in reversed(category_list): + combined = category + combined + if combined in category_file: + category_parameters = category_file[combined] + break + if category in category_file: + category_parameters = category_file[category] + break + combined = '/' + combined + if not category_parameters: + return + print(category_parameters) + if 'parent' in category_parameters: + for parent in category_parameters['parent']: + find_parameters(output_dict, [parent]) + del category_parameters['parent'] + + for parameter in category_parameters.keys(): + if category_parameters[parameter]: + for supplier_parameter in category_parameters[parameter]: + output_dict[supplier_parameter] = parameter + print(output_dict) + try: category_file = load_file(supplier_config_path) except: return None - category_parameters = None - for category in reversed(categories): - try: - category_parameters = category_file[category] - break - except: - pass - if not category_parameters: - return None - category_parameters_inversed = {} - for parameter in category_parameters.keys(): - if category_parameters[parameter]: - for supplier_parameter in category_parameters[parameter]: - category_parameters_inversed[supplier_parameter] = parameter + + find_parameters(category_parameters_inversed, categories) # print(category_parameters_inversed) return category_parameters_inversed diff --git a/kintree/config/inventree/supplier_parameters.yaml b/kintree/config/inventree/supplier_parameters.yaml index 51d9d2f7..d1a969f8 100644 --- a/kintree/config/inventree/supplier_parameters.yaml +++ b/kintree/config/inventree/supplier_parameters.yaml @@ -2,7 +2,18 @@ # Each template parameter can match to multiple suppliers parameters # Categories (main keys) should match categories in the categories.yaml file # Parameter template names should match those found in the parameters.yaml file +Base: + Temperature Range: + - Operating Temperature + Package Type: + - Package / Case +Passives: + Tolerance: + - Tolerance Capacitors: + parent: + - Base + - Passives ESR: - ESR (Equivalent Series Resistance) Package Height: @@ -10,8 +21,6 @@ Capacitors: - Thickness (Max) Package Size: - Size / Dimension - Package Type: - - Package / Case Rated Voltage: - Voltage - Rated - Voltage Rated @@ -19,19 +28,17 @@ Capacitors: - Temperature Coefficient Temperature Range: - Operating Temperature - Tolerance: - - Tolerance Value: - Capacitance Circuit Protections: + parent: + - Base Breakdown Voltage: - Voltage - Breakdown (Min) Capacitance: - Capacitance @ Frequency Clamping Voltage: - Voltage - Clamping (Max) @ Ipp - Package Type: - - Package / Case Rated Current: - Current Rating (Amps) - Current - Max @@ -42,8 +49,6 @@ Circuit Protections: - Voltage - Max Standoff Voltage: - Voltage - Reverse Standoff (Typ) - Temperature Range: - - Operating Temperature Value: - Manufacturer Part Number Connectors: @@ -81,6 +86,8 @@ Connectors: Value: - Manufacturer Part Number Crystals and Oscillators: + parent: + - Base Frequency Stability: - Frequency Stability Frequency Tolerance: @@ -91,17 +98,15 @@ Crystals and Oscillators: - Height - Seated (Max) Package Size: - Size / Dimension - Package Type: - - Package / Case Rated Current: - Current - Supply (Max) Rated Voltage: - Voltage - Supply - Temperature Range: - - Operating Temperature Value: - Frequency Diodes: + parent: + - Base Forward Voltage: - Voltage - Forward (Vf) (Max) @ If - Voltage - Forward (Vf) (Typ) @@ -109,8 +114,6 @@ Diodes: - Diode Type LED Color: - Color - Package Type: - - Package / Case Rated Current: - Current - Average Rectified (Io) Rated Power: @@ -119,11 +122,13 @@ Diodes: - Voltage - DC Reverse (Vr) (Max) - Voltage - Zener (Nom) (Vz) Temperature Range: - - Operating Temperature - Operating Temperature - Junction Value: - Manufacturer Part Number Inductors: + parent: + - Base + - Passives ESR: - DC Resistance (DCR) - DC Resistance (DCR) (Max) @@ -132,8 +137,6 @@ Inductors: - Height (Max) Package Size: - Size / Dimension - Package Type: - - Package / Case Rated Current: - Current Rating (Max) - Current Rating (Amps) @@ -141,14 +144,12 @@ Inductors: - Current - Saturation Shielding: - Shielding - Temperature Range: - - Operating Temperature - Tolerance: - - Tolerance Value: - Inductance - Impedance @ Frequency Integrated Circuits: + parent: + - Base Frequency: - Clock Frequency - Speed @@ -166,8 +167,6 @@ Integrated Circuits: - Memory Size Number of Channels: - Channels per Circuit - Package Type: - - Package / Case Rated Voltage: - Voltage - VCCA - Voltage - VCCB @@ -175,11 +174,11 @@ Integrated Circuits: - Voltage - Supply (Vcc/Vdd) - Voltage - Supply, Digital - Voltage - Supply, Single (V+) - Temperature Range: - - Operating Temperature Value: - Manufacturer Part Number Mechanicals: + parent: + - Base Function Type: - Circuit - Type @@ -195,11 +194,11 @@ Mechanicals: - Screw, Thread Size Rated Current: - Contact Rating @ Voltage - Temperature Range: - - Operating Temperature Value: - Manufacturer Part Number Power Management: + parent: + - Base (Min) Output Voltage: - Voltage - Output (Min/Fixed) Frequency: @@ -220,30 +219,26 @@ Power Management: - Current - Quiescent (Iq) Rated Current: - Current - Output - Temperature Range: - - Operating Temperature Value: - Manufacturer Part Number RF: + parent: + - Base Frequency: - Frequency Range Function Type: null - Package Type: - - Package / Case Rated Voltage: null - Temperature Range: - - Operating Temperature Value: - Manufacturer Part Number Resistors: + parent: + - Passives Package Type: - Supplier Device Package Rated Power: - Power (Watts) Temperature Range: - Operating Temperature - Tolerance: - - Tolerance Value: - Resistance Transistors: From e455d421736e2cc03db751b1b59335f9354fc896 Mon Sep 17 00:00:00 2001 From: T0jan Date: Fri, 16 Feb 2024 14:41:41 +0100 Subject: [PATCH 2/6] add way to support template images --- README.md | 2 ++ kintree/config/config_interface.py | 3 --- kintree/database/inventree_api.py | 21 ++++++++++++++------- kintree/database/inventree_interface.py | 21 +++++++++++++++------ 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index ee86ba23..2c456731 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,8 @@ CATEGORY_NAME: It is also possible to cross reference the mappings of different categories. To define one or multiple parent categories a parameter named `parent` can be added where the items then are the desired parent categories. If a parameter name is present in both parent and child, the childs definition will override the parent. +A template image for an category can be set by using the `image` parameter. The sole item in this parameter must the filename of an already existing part image on the the InvenTree server. + Refer to [this file](https://github.com/sparkmicro/Ki-nTree/blob/main/kintree/config/inventree/supplier_parameters.yaml) as a starting point / example. #### Part Number Search diff --git a/kintree/config/config_interface.py b/kintree/config/config_interface.py index 594887f7..234352f3 100644 --- a/kintree/config/config_interface.py +++ b/kintree/config/config_interface.py @@ -447,7 +447,6 @@ def find_parameters(output_dict, category_list): combined = '/' + combined if not category_parameters: return - print(category_parameters) if 'parent' in category_parameters: for parent in category_parameters['parent']: find_parameters(output_dict, [parent]) @@ -457,7 +456,6 @@ def find_parameters(output_dict, category_list): if category_parameters[parameter]: for supplier_parameter in category_parameters[parameter]: output_dict[supplier_parameter] = parameter - print(output_dict) try: category_file = load_file(supplier_config_path) @@ -467,7 +465,6 @@ def find_parameters(output_dict, category_list): find_parameters(category_parameters_inversed, categories) - # print(category_parameters_inversed) return category_parameters_inversed diff --git a/kintree/database/inventree_api.py b/kintree/database/inventree_api.py index 5b9cbb6a..616ca673 100644 --- a/kintree/database/inventree_api.py +++ b/kintree/database/inventree_api.py @@ -177,11 +177,8 @@ def get_part_info(part_id: int) -> str: def set_part_number(part_id: int, ipn: str) -> bool: ''' Set InvenTree part number for specified Part ID ''' - global inventree_api - - part = Part(inventree_api, part_id) - part._data['IPN'] = ipn - part.save() + data={'IPN': ipn} + update_part(part_id, data) if Part(inventree_api, part_id).IPN == ipn: return True @@ -409,7 +406,7 @@ def upload_part_datasheet(datasheet_url: str, part_id: int) -> str: return '' -def create_part(category_id: int, name: str, description: str, revision: str, ipn: str, keywords=None) -> int: +def create_part(category_id: int, name: str, description: str, revision: str, ipn: str, keywords=None, **kwargs) -> int: ''' Create InvenTree part ''' global inventree_api @@ -425,7 +422,7 @@ def create_part(category_id: int, name: str, description: str, revision: str, ip 'virtual': False, 'component': True, 'purchaseable': True, - }) + } | kwargs) except Exception: cprint('[TREE]\tError: Part creation failed. Check if Ki-nTree settings match InvenTree part settings.', silent=settings.SILENT) return 0 @@ -435,6 +432,16 @@ def create_part(category_id: int, name: str, description: str, revision: str, ip else: return 0 +def update_part(pk: int, data: dict) -> int: + '''Update an existing parts data''' + global inventree_api + + part = Part(inventree_api, pk) + if part: + part.save(data=data) + return part.pk + else: + return 0 def create_company(company_name: str, manufacturer=False, supplier=False) -> bool: ''' Create InvenTree company ''' diff --git a/kintree/database/inventree_interface.py b/kintree/database/inventree_interface.py index 27992491..0605018c 100644 --- a/kintree/database/inventree_interface.py +++ b/kintree/database/inventree_interface.py @@ -286,7 +286,11 @@ def translate_form_to_inventree(part_info: dict, category_tree: list, is_custom= for supplier_param, inventree_param in parameter_map.items(): # Some parameters may not be mapped if inventree_param not in inventree_part['parameters'].keys(): - if supplier_param != 'Manufacturer Part Number': + if supplier_param == 'Manufacturer Part Number': + inventree_part['parameters'][inventree_param] = part_info['manufacturer_part_number'] + elif inventree_param == 'image': + inventree_part['existing_image'] = supplier_param + else: try: parameter_value = part_tools.clean_parameter_value( category=category_tree[0], @@ -296,9 +300,6 @@ def translate_form_to_inventree(part_info: dict, category_tree: list, is_custom= inventree_part['parameters'][inventree_param] = parameter_value except KeyError: parameters_missing.append(supplier_param) - else: - inventree_part['parameters'][inventree_param] = part_info['manufacturer_part_number'] - if parameters_missing: msg = '[INFO]\tWarning: The following parameters were not found in supplier data:\n' msg += str(parameters_missing) @@ -306,6 +307,8 @@ def translate_form_to_inventree(part_info: dict, category_tree: list, is_custom= # Check for missing InvenTree parameters and fill value with dash for inventree_param in parameter_map.values(): + if inventree_param == 'image': + continue if inventree_param not in inventree_part['parameters'].keys(): inventree_part['parameters'][inventree_param] = '-' @@ -550,6 +553,7 @@ def inventree_create(part_info: dict, kicad=False, symbol=None, footprint=None, description=inventree_part['description'], revision=inventree_part['revision'], keywords=inventree_part['keywords'], + existing_image=inventree_part.get('existing_image', ''), ipn=ipn) # Check part primary key @@ -588,7 +592,7 @@ def inventree_create(part_info: dict, kicad=False, symbol=None, footprint=None, if part_pk > 0: if new_part: cprint('[INFO]\tSuccess: Added new part to InvenTree', silent=settings.SILENT) - if inventree_part['image']: + if inventree_part['image'] and not inventree_part['existing_image']: # Add image image_result = inventree_api.upload_part_image(inventree_part['image'], part_pk) if not image_result: @@ -752,6 +756,7 @@ def inventree_create_alternate(part_info: dict, part_id='', part_ipn='', show_pr # Translate to InvenTree part format category_tree = inventree_api.get_category_tree(part.category) category_tree = list(category_tree.values()) + category_tree.reverse() inventree_part = translate_form_to_inventree( part_info=part_info, category_tree=category_tree, @@ -760,7 +765,11 @@ def inventree_create_alternate(part_info: dict, part_id='', part_ipn='', show_pr # If the part has no image yet try to upload it from the data if not part.image: image = part_info.get('image', '') - if image: + existing_image = inventree_part.get('existing_image', '') + if existing_image: + inventree_api.update_part(pk=part_pk, + data={'existing_image': existing_image}) + elif image: inventree_api.upload_part_image(image_url=image, part_id=part_pk) # create or update parameters From a7a5134a76e9ea69185253041cf635ee9eb3de71 Mon Sep 17 00:00:00 2001 From: T0jan Date: Mon, 19 Feb 2024 16:27:44 +0100 Subject: [PATCH 3/6] fix linting issues --- kintree/config/config_interface.py | 3 ++- kintree/database/inventree_api.py | 8 +++++--- kintree/database/inventree_interface.py | 7 +++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/kintree/config/config_interface.py b/kintree/config/config_interface.py index 234352f3..06edab76 100644 --- a/kintree/config/config_interface.py +++ b/kintree/config/config_interface.py @@ -437,7 +437,8 @@ def find_parameters(output_dict, category_list): category_parameters = None combined = '' for category in reversed(category_list): - combined = category + combined + if category: + combined = category + combined if combined in category_file: category_parameters = category_file[combined] break diff --git a/kintree/database/inventree_api.py b/kintree/database/inventree_api.py index 616ca673..ee3cc0bb 100644 --- a/kintree/database/inventree_api.py +++ b/kintree/database/inventree_api.py @@ -177,7 +177,7 @@ def get_part_info(part_id: int) -> str: def set_part_number(part_id: int, ipn: str) -> bool: ''' Set InvenTree part number for specified Part ID ''' - data={'IPN': ipn} + data = {'IPN': ipn} update_part(part_id, data) if Part(inventree_api, part_id).IPN == ipn: @@ -406,7 +406,7 @@ def upload_part_datasheet(datasheet_url: str, part_id: int) -> str: return '' -def create_part(category_id: int, name: str, description: str, revision: str, ipn: str, keywords=None, **kwargs) -> int: +def create_part(category_id: int, name: str, description: str, revision: str, ipn: str, keywords=None) -> int: ''' Create InvenTree part ''' global inventree_api @@ -422,7 +422,7 @@ def create_part(category_id: int, name: str, description: str, revision: str, ip 'virtual': False, 'component': True, 'purchaseable': True, - } | kwargs) + }) except Exception: cprint('[TREE]\tError: Part creation failed. Check if Ki-nTree settings match InvenTree part settings.', silent=settings.SILENT) return 0 @@ -432,6 +432,7 @@ def create_part(category_id: int, name: str, description: str, revision: str, ip else: return 0 + def update_part(pk: int, data: dict) -> int: '''Update an existing parts data''' global inventree_api @@ -443,6 +444,7 @@ def update_part(pk: int, data: dict) -> int: else: return 0 + def create_company(company_name: str, manufacturer=False, supplier=False) -> bool: ''' Create InvenTree company ''' global inventree_api diff --git a/kintree/database/inventree_interface.py b/kintree/database/inventree_interface.py index 0605018c..1fa8f13e 100644 --- a/kintree/database/inventree_interface.py +++ b/kintree/database/inventree_interface.py @@ -553,7 +553,6 @@ def inventree_create(part_info: dict, kicad=False, symbol=None, footprint=None, description=inventree_part['description'], revision=inventree_part['revision'], keywords=inventree_part['keywords'], - existing_image=inventree_part.get('existing_image', ''), ipn=ipn) # Check part primary key @@ -592,7 +591,11 @@ def inventree_create(part_info: dict, kicad=False, symbol=None, footprint=None, if part_pk > 0: if new_part: cprint('[INFO]\tSuccess: Added new part to InvenTree', silent=settings.SILENT) - if inventree_part['image'] and not inventree_part['existing_image']: + if inventree_part.get('existing_image', ''): + inventree_api.update_part( + part_pk, + data={'existing_image': inventree_part['existing_image']}) + elif inventree_part['image']: # Add image image_result = inventree_api.upload_part_image(inventree_part['image'], part_pk) if not image_result: From 7329a97d0052679568af53177c6a54e2da348626 Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 27 Feb 2024 09:59:50 -0500 Subject: [PATCH 4/6] Fix @martonmiklos stock implementation conflicts and try it out --- kintree/config/inventree/stock_locations.yaml | 1 + kintree/config/settings.py | 1 + kintree/database/inventree_api.py | 106 +++++++++++++++ kintree/database/inventree_interface.py | 52 +++++++- kintree/gui/views/main.py | 123 +++++++++++++++++- 5 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 kintree/config/inventree/stock_locations.yaml diff --git a/kintree/config/inventree/stock_locations.yaml b/kintree/config/inventree/stock_locations.yaml new file mode 100644 index 00000000..0758ed3b --- /dev/null +++ b/kintree/config/inventree/stock_locations.yaml @@ -0,0 +1 @@ +STOCK_LOCATIONS: null \ No newline at end of file diff --git a/kintree/config/settings.py b/kintree/config/settings.py index 8e7c93ca..03e746de 100644 --- a/kintree/config/settings.py +++ b/kintree/config/settings.py @@ -83,6 +83,7 @@ def load_user_config(): # Inventree CONFIG_CATEGORIES = os.path.join(CONFIG_USER_FILES, 'categories.yaml') +CONFIG_STOCK_LOCATIONS = os.path.join(CONFIG_USER_FILES, 'stock_locations.yaml') CONFIG_PARAMETERS = os.path.join(CONFIG_USER_FILES, 'parameters.yaml') CONFIG_PARAMETERS_FILTERS = os.path.join( CONFIG_USER_FILES, 'parameters_filters.yaml') diff --git a/kintree/database/inventree_api.py b/kintree/database/inventree_api.py index ee3cc0bb..35eab299 100644 --- a/kintree/database/inventree_api.py +++ b/kintree/database/inventree_api.py @@ -19,6 +19,8 @@ from inventree.company import Company, ManufacturerPart, SupplierPart, SupplierPriceBreak from inventree.part import Part, PartCategory, Parameter, ParameterTemplate from inventree.currency import CurrencyManager +from inventree.stock import StockLocation +from inventree.stock import StockItem def connect(server: str, @@ -91,6 +93,40 @@ def get_inventree_category_id(category_tree: list) -> int: return -1 +def get_inventree_stock_location_id(stock_location_tree: list) -> int: + ''' Get InvenTree stock location ID from name, specificy parent if subcategory ''' + global inventree_api + + # Fetch all categories + stock_locations = StockLocation.list(inventree_api, name=stock_location_tree[-1]) + if len(stock_locations) == 1: + return stock_locations[0].pk + else: + if len(stock_location_tree) > 1: + # Match the parent category + parent_stock_location_id = get_inventree_category_id(stock_location_tree[:-1]) + if parent_stock_location_id: + for location in stock_locations: + try: + if parent_stock_location_id == location.getParentLocation().pk: + return location.pk + except AttributeError: + pass + # # Check parent id match (if passed as argument) + # match = True + # if parent_stock_location_id: + # cprint(f'[TREE]\t{item.getParentCategory().pk} ?= {parent_stock_location_id}', silent=settings.HIDE_DEBUG) + # if item.getParentCategory().pk != parent_stock_location_id: + # match = False + # if match: + # cprint(f'[TREE]\t{item.name} ?= {category_name} => True', silent=settings.HIDE_DEBUG) + # return item.pk + # else: + # cprint(f'[TREE]\t{item.name} ?= {category_name} => False', silent=settings.HIDE_DEBUG) + + return -1 + + def get_categories() -> dict: '''Fetch InvenTree categories''' global inventree_api @@ -126,6 +162,41 @@ def deep_add(tree: dict, keys: list, item: dict): return categories +def get_stock_locations() -> dict: + '''Fetch InvenTree stock locations''' + global inventree_api + + categories = {} + # Get all categories (list) + db_categories = StockLocation.list(inventree_api) + + def deep_add(tree: dict, keys: list, item: dict): + if len(keys) == 1: + try: + tree[keys[0]].update(item) + except (KeyError, AttributeError): + tree[keys[0]] = item + return + return deep_add(tree.get(keys[0]), keys[1:], item) + + for category in db_categories: + parent = category.getParentLocation() + children = category.getChildLocations() + + if not parent and not children: + categories[category.name] = None + continue + elif parent: + parent_list = [] + while parent: + parent_list.insert(0, parent.name) + parent = parent.getParentLocation() + cat = {category.name: None} + deep_add(categories, parent_list, cat) + + return categories + + def get_category_tree(category_id: int) -> dict: ''' Get all parents of a category''' category = PartCategory(inventree_api, category_id) @@ -138,6 +209,22 @@ def get_category_tree(category_id: int) -> dict: return category_list +def get_stock_location_tree(id: int) -> dict: + ''' Get all parents of a stock_location''' + location = StockLocation(inventree_api, id) + list = {id: location.name} + + while location.parent: + location = location.getParentLocation() + list[location.pk] = location.name + + return list + + +def create_stock(stock_data: dict) -> dict: + return StockItem.create(inventree_api, stock_data) + + def get_category_parameters(category_id: int) -> list: ''' Get all default parameter templates for category ''' global inventree_api @@ -433,6 +520,18 @@ def create_part(category_id: int, name: str, description: str, revision: str, ip return 0 +def set_part_default_location(part_pk: int, location_pk: int): + global inventree_api + + # Retrieve part instance with primary-key of 1 + part = Part(inventree_api, pk=part_pk) + + # Update specified part parameters + part.save(data={ + "default_location": location_pk, + }) + + def update_part(pk: int, data: dict) -> int: '''Update an existing parts data''' global inventree_api @@ -648,6 +747,13 @@ def create_supplier_part(part_id: int, manufacturer_name: str, manufacturer_mpn: return False, False +def sanitize_price(price_in): + price = re.findall('\d+.\d+', price_in)[0] + price = price.replace(',', '.') + price = price.replace('\xa0', '') + return price + + def update_price_breaks(supplier_part, price_breaks: dict, currency='USD') -> bool: diff --git a/kintree/database/inventree_interface.py b/kintree/database/inventree_interface.py index 1fa8f13e..32ebadeb 100644 --- a/kintree/database/inventree_interface.py +++ b/kintree/database/inventree_interface.py @@ -100,6 +100,46 @@ def build_tree(tree, left_to_go, level) -> list: return inventree_categories +def build_stock_location_tree(reload=False, location=None) -> dict: + '''Build InvenTree stock locations tree from database data''' + + locations_data = config_interface.load_file(settings.CONFIG_STOCK_LOCATIONS) + + def build_tree(tree, left_to_go, level) -> list: + try: + last_entry = f' {category_tree(tree[-1])}{category_separator}' + except IndexError: + last_entry = '' + if isinstance(left_to_go, dict): + for key, value in left_to_go.items(): + tree.append(f'{"-" * level}{last_entry}{key}') + build_tree(tree, value, level + 1) + elif isinstance(left_to_go, list): + # Supports legacy structure + for item in left_to_go: + tree.append(f'{"-" * level}{last_entry}{item}') + elif left_to_go is None: + pass + return + + if reload: + stock_locations = inventree_api.get_stock_locations() + locations_data.update({'STOCK_LOCATIONS': stock_locations}) + config_interface.dump_file(locations_data, settings.CONFIG_STOCK_LOCATIONS) + else: + stock_locations = locations_data.get('STOCK_LOCATIONS', {}) + + # Get specified branch + if location: + stock_locations = {location: stock_locations.get(location, {})} + + inventree_stock_locations = [] + # Build category tree + build_tree(inventree_stock_locations, stock_locations, 0) + + return inventree_stock_locations + + def get_categories_from_supplier_data(part_info: dict, supplier_only=False) -> list: ''' Find categories from part supplier data, use "somewhat automatic" matching ''' from thefuzz import fuzz @@ -494,7 +534,11 @@ def inventree_create_supplier_part(part) -> bool: return -def inventree_create(part_info: dict, kicad=False, symbol=None, footprint=None, show_progress=True, is_custom=False): +def get_inventree_stock_location_id(stock_location_tree: list): + return inventree_api.get_inventree_stock_location_id(stock_location_tree) + + +def inventree_create(part_info: dict, stock=None, kicad=False, symbol=None, footprint=None, show_progress=True, is_custom=False): ''' Create InvenTree part from supplier part data and categories ''' part_pk = 0 @@ -701,6 +745,12 @@ def inventree_create(part_info: dict, kicad=False, symbol=None, footprint=None, price_breaks=inventree_part['pricing'], currency=inventree_part['currency']) + if stock is not None: + stock['part'] = part_pk + inventree_api.create_stock(stock) + if stock['make_default']: + inventree_api.set_part_default_location(part_pk, stock['location']) + # Progress Update if not progress.update_progress_bar(show_progress): pass diff --git a/kintree/gui/views/main.py b/kintree/gui/views/main.py index a8f24788..45621433 100644 --- a/kintree/gui/views/main.py +++ b/kintree/gui/views/main.py @@ -510,6 +510,13 @@ class InventreeView(MainView): icon=ft.icons.REPLAY, disabled=False, ), + 'load_stock_locations': ft.ElevatedButton( + 'Reload InvenTree Stock locations', + width=GUI_PARAMS['button_width'] * 2.8, + height=36, + icon=ft.icons.REPLAY, + disabled=False, + ), 'Category': DropdownWithSearch( label='Category', dr_width=GUI_PARAMS['textfield_width'], @@ -556,12 +563,36 @@ class InventreeView(MainView): value=settings.UPDATE_INVENTREE if settings.ENABLE_INVENTREE else False, disabled=not settings.ENABLE_INVENTREE, ), + 'Create stock': SwitchWithRefs( + label='Create Stock', + disabled=not settings.ENABLE_INVENTREE, + ), + 'Stock location': DropdownWithSearch( + label='Stock location', + disabled=not settings.ENABLE_INVENTREE, + dr_width=GUI_PARAMS['textfield_width'], + sr_width=GUI_PARAMS['searchfield_width'], + dense=GUI_PARAMS['textfield_dense'], + options=[], + ), + 'Stock quantity': ft.TextField( + label='Stock quantity', + disabled=not settings.ENABLE_INVENTREE, + keyboard_type=ft.KeyboardType.NUMBER, + value="1", + ), + 'Make stock location default': ft.Checkbox( + label='Make this the part\'s default location', + disabled=not settings.ENABLE_INVENTREE, + value=True, + ), } def __init__(self, page: ft.Page): self.category_row_ref = ft.Ref[ft.Row]() self.ipncode_row_ref = ft.Ref[ft.Row]() self.alternate_row_ref = ft.Ref[ft.Row]() + self.create_stock_widgets_ref = ft.Ref[ft.Row]() super().__init__(page) def partial_update(self): @@ -572,6 +603,9 @@ def sanitize_data(self): category_tree = self.data.get('Category', None) if category_tree: self.data['Category'] = inventree_interface.split_category_tree(category_tree) + stock_location_tree = self.data.get('Stock location', None) + if stock_location_tree: + self.data['Stock location'] = inventree_interface.split_category_tree(stock_location_tree) def process_enable(self, e): inventree_enable = True @@ -585,9 +619,12 @@ def process_enable(self, e): self.fields['alternate'].value = inventree_enable self.fields['alternate'].update() self.process_alternate(e, value=inventree_enable) + self.process_create_stock(e, value=inventree_enable) else: alternate_enable = self.fields['alternate'].value self.process_alternate(e, value=alternate_enable) + stock_create_enabled = self.fields['Create stock'].value + self.process_create_stock(e, value=stock_create_enabled) self.process_ipncode() @@ -668,6 +705,10 @@ def process_category(self, e=None, label=None, value=None): self.fields['IPN: Category Code'].update() self.push_data(e) + def process_location(self, e=None, label=None, value=None): + self.fields['Stock location'].options = self.get_stock_location_options() + self.push_data(e) + def process_ipncode(self): ipncode_enable = bool( settings.CONFIG_IPN.get('IPN_ENABLE_CREATE', False) and settings.CONFIG_IPN.get('IPN_CATEGORY_CODE', False) @@ -675,6 +716,20 @@ def process_ipncode(self): self.ipncode_row_ref.current.visible = ipncode_enable self.ipncode_row_ref.current.update() + def process_create_stock(self, e, value=None): + if value is not None: + create_stock_visible = value + else: + self.fields['New Category Code'].visible = False + # Get switch value + create_stock_visible = False + if e.data.lower() == 'true': + create_stock_visible = True + + # Stock create row visibility + self.create_stock_widgets_ref.current.visible = create_stock_visible + self.create_stock_widgets_ref.current.update() + def get_code_options(self): try: return [ @@ -689,6 +744,12 @@ def get_category_options(self, reload=False): ft.dropdown.Option(category) for category in inventree_interface.build_category_tree(reload=reload) ] + + def get_stock_location_options(self, reload=False): + return [ + ft.dropdown.Option(location) + for location in inventree_interface.build_stock_location_tree(reload=reload) + ] def reload_categories(self, e): self.page.splash.visible = True @@ -704,6 +765,20 @@ def reload_categories(self, e): self.page.splash.visible = False self.page.update() + def reload_stock_locations(self, e): + self.page.splash.visible = True + self.page.update() + + # Check connection + if not inventree_interface.connect_to_server(): + self.show_dialog(DialogType.ERROR, 'ERROR: Failed to connect to InvenTree server') + else: + self.fields['Stock location'].options = self.get_stock_location_options(reload=True) + self.fields['Stock location'].update() + + self.page.splash.visible = False + self.page.update() + def create_ipn_code(self, e): # Get switch value new_code = True @@ -734,6 +809,12 @@ def build_column(self): self.fields['Existing Part ID'].on_change = self.push_data self.fields['Existing Part IPN'].on_change = self.push_data self.fields['Update Parameter'].on_change = self.process_update + # Create stock location + self.fields['Stock location'].options = self.get_stock_location_options() + self.fields['Stock location'].on_change = self.process_location + self.fields["Create stock"].on_change = self.process_create_stock + self.fields['Stock location'].on_change = self.push_data + self.fields['load_stock_locations'].on_click = self.reload_stock_locations self.column = ft.Column( controls=[ @@ -792,6 +873,31 @@ def build_column(self): ) ] ), + ft.Column( + ref=self.create_stock_widgets_ref, + controls=[ + ft.Row( + controls=[ + self.fields['Create stock'], + self.fields['load_stock_locations'] + ] + ) + ] + ), + ft.Column( + ref=self.create_stock_widgets_ref, + controls=[ + ft.Row( + controls=[self.fields['Stock location']], + ), + ft.Row( + controls=[self.fields['Stock quantity']], + ), + ft.Row( + controls=[self.fields['Make stock location default']], + ), + ] + ) ], ) @@ -1299,7 +1405,21 @@ def create_part(self, e=None): part_info['category_code'] = data_from_views['InvenTree'].get('New Category Code', '') else: part_info['category_code'] = data_from_views['InvenTree'].get('IPN: Category Code', '') - + + stock = None + if data_from_views['InvenTree'].get('Create stock'): + stock_tree = data_from_views['InvenTree'].get('Stock location', None) + if not stock_tree: + # Check category is present + self.show_dialog(DialogType.ERROR, 'Missing InvenTree Stock location') + return + + stock = { + 'location': inventree_interface.get_inventree_stock_location_id(data_from_views['InvenTree'].get('Stock location')), + 'quantity': data_from_views['InvenTree'].get('Stock quantity'), + 'make_default': data_from_views['InvenTree'].get('Make stock location default'), + } + # Create new part new_part, part_pk, part_info = inventree_interface.inventree_create( part_info=part_info, @@ -1308,6 +1428,7 @@ def create_part(self, e=None): footprint=footprint, show_progress=self.fields['inventree_progress'], is_custom=custom, + stock=stock, ) # print(new_part, part_pk) # cprint(part_info) From 501263146ccf6961dc165d887544e3cfd2bb4251 Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 27 Feb 2024 10:17:41 -0500 Subject: [PATCH 5/6] Add platform check for YAML dump --- kintree/config/config_interface.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/kintree/config/config_interface.py b/kintree/config/config_interface.py index 06edab76..b43bc220 100644 --- a/kintree/config/config_interface.py +++ b/kintree/config/config_interface.py @@ -1,6 +1,7 @@ import base64 import copy import os +from sys import platform import yaml from ..common.tools import cprint @@ -28,7 +29,10 @@ def dump_file(data: dict, file_path: str) -> bool: ''' Safe dump YAML file ''' with open(file_path, 'w') as file: try: - yaml.safe_dump(data, file, default_flow_style=False, allow_unicode=True) + if platform == "win32": + yaml.safe_dump(data, file, default_flow_style=False) + else: + yaml.safe_dump(data, file, default_flow_style=False, allow_unicode=True) except yaml.YAMLError as exc: print(exc) return False From 4d12268125a3f69ff9485363046f3f62aea8591d Mon Sep 17 00:00:00 2001 From: eeintech Date: Tue, 5 Mar 2024 15:32:54 -0500 Subject: [PATCH 6/6] Uncheck default stock location --- kintree/gui/views/main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/kintree/gui/views/main.py b/kintree/gui/views/main.py index 45621433..45e6bac2 100644 --- a/kintree/gui/views/main.py +++ b/kintree/gui/views/main.py @@ -568,7 +568,7 @@ class InventreeView(MainView): disabled=not settings.ENABLE_INVENTREE, ), 'Stock location': DropdownWithSearch( - label='Stock location', + label='Stock Location', disabled=not settings.ENABLE_INVENTREE, dr_width=GUI_PARAMS['textfield_width'], sr_width=GUI_PARAMS['searchfield_width'], @@ -576,15 +576,15 @@ class InventreeView(MainView): options=[], ), 'Stock quantity': ft.TextField( - label='Stock quantity', + label='Stock Quantity', disabled=not settings.ENABLE_INVENTREE, keyboard_type=ft.KeyboardType.NUMBER, - value="1", + value='1', ), 'Make stock location default': ft.Checkbox( - label='Make this the part\'s default location', + label="Set this location as the part\'s default location", disabled=not settings.ENABLE_INVENTREE, - value=True, + value=False, ), }