mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 15:15:42 +00:00 
			
		
		
		
	Custom migration for walking user through the process of mapping supplierpart to manufacturer
(cherry picked from commit 290002fe9d)
			
			
This commit is contained in:
		
							
								
								
									
										264
									
								
								InvenTree/company/migrations/0019_auto_20200413_0642.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								InvenTree/company/migrations/0019_auto_20200413_0642.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,264 @@
 | 
			
		||||
# Generated by Django 2.2.10 on 2020-04-13 06:42
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
from rapidfuzz import fuzz
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
from company.models import Company, SupplierPart
 | 
			
		||||
from django.db.utils import OperationalError, ProgrammingError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def clear():
 | 
			
		||||
    os.system('cls' if os.name == 'nt' else 'clear')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def reverse_association(apps, schema_editor):
 | 
			
		||||
    """
 | 
			
		||||
    This is the 'reverse' operation of the manufacturer reversal.
 | 
			
		||||
    This operation is easier:
 | 
			
		||||
 | 
			
		||||
    For each SupplierPart object, copy the name of the 'manufacturer' field
 | 
			
		||||
    into the 'manufacturer_name' field.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    print("Reversing migration for manufacturer association")
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        for part in SupplierPart.objects.all():
 | 
			
		||||
            if part.manufacturer is not None:
 | 
			
		||||
                part.manufacturer_name = part.manufacturer.name
 | 
			
		||||
                
 | 
			
		||||
                part.save()
 | 
			
		||||
 | 
			
		||||
    except (OperationalError, ProgrammingError):
 | 
			
		||||
        # An exception might be called if the database is empty
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def associate_manufacturers(apps, schema_editor):
 | 
			
		||||
    """
 | 
			
		||||
    This migration is the "middle step" in migration of the "manufacturer" field for the SupplierPart model.
 | 
			
		||||
    
 | 
			
		||||
    Previously the "manufacturer" field was a simple text field with the manufacturer name.
 | 
			
		||||
    This is quite insufficient.
 | 
			
		||||
    The new "manufacturer" field is a link to Company object which has the "is_manufacturer" parameter set to True
 | 
			
		||||
 | 
			
		||||
    This migration requires user interaction to create new "manufacturer" Company objects,
 | 
			
		||||
    based on the text value in the "manufacturer_name" field (which was created in the previous migration).
 | 
			
		||||
 | 
			
		||||
    It uses fuzzy pattern matching to help the user out as much as possible.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    # Link a 'manufacturer_name' to a 'Company'
 | 
			
		||||
    links = {}
 | 
			
		||||
 | 
			
		||||
    # Map company names to company objects
 | 
			
		||||
    companies = {}
 | 
			
		||||
 | 
			
		||||
    for company in Company.objects.all():
 | 
			
		||||
        companies[company.name] = company
 | 
			
		||||
 | 
			
		||||
    # List of parts which will need saving
 | 
			
		||||
    parts = []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def link_part(part, name):
 | 
			
		||||
        """ Attempt to link Part to an existing Company """
 | 
			
		||||
 | 
			
		||||
        # Matches a company name directly
 | 
			
		||||
        if name in companies.keys():
 | 
			
		||||
            print(" -> '{n}' maps to existing manufacturer".format(n=name))
 | 
			
		||||
            part.manufacturer = companies[name]
 | 
			
		||||
            part.save()
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        # Have we already mapped this 
 | 
			
		||||
        if name in links.keys():
 | 
			
		||||
            print(" -> Mapped '{n}' -> '{c}'".format(n=name, c=links[name].name))
 | 
			
		||||
            part.manufacturer = links[name]
 | 
			
		||||
            part.save()
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        # Mapping not possible
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def create_manufacturer(part, input_name, company_name):
 | 
			
		||||
        """ Create a new manufacturer """
 | 
			
		||||
 | 
			
		||||
        company = Company(name=company_name, description=company_name, is_manufacturer=True)
 | 
			
		||||
 | 
			
		||||
        company.is_manufacturer = True
 | 
			
		||||
 | 
			
		||||
        # Map both names to the same company
 | 
			
		||||
        links[input_name] = company
 | 
			
		||||
        links[company_name] = company
 | 
			
		||||
 | 
			
		||||
        companies[company_name] = company
 | 
			
		||||
 | 
			
		||||
        # Save the company BEFORE we associate the part, otherwise the PK does not exist
 | 
			
		||||
        company.save()
 | 
			
		||||
        
 | 
			
		||||
        # Save the manufacturer reference link
 | 
			
		||||
        part.manufacturer = company
 | 
			
		||||
        part.save()
 | 
			
		||||
 | 
			
		||||
        print(" -> Created new manufacturer: '{name}'".format(name=company_name))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def find_matches(text, threshold=65):
 | 
			
		||||
        """
 | 
			
		||||
        Attempt to match a 'name' to an existing Company.
 | 
			
		||||
        A list of potential matches will be returned.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        matches = []
 | 
			
		||||
 | 
			
		||||
        for name in companies.keys():
 | 
			
		||||
            # Case-insensitive matching
 | 
			
		||||
            ratio = fuzz.partial_ratio(name.lower(), text.lower())
 | 
			
		||||
 | 
			
		||||
            if ratio > threshold:
 | 
			
		||||
                matches.append({'name': name, 'match': ratio})
 | 
			
		||||
 | 
			
		||||
        if len(matches) > 0:
 | 
			
		||||
            return [match['name'] for match in sorted(matches, key=lambda item: item['match'], reverse=True)]
 | 
			
		||||
        else:
 | 
			
		||||
            return []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def map_part_to_manufacturer(part, idx, total):
 | 
			
		||||
 | 
			
		||||
        name = str(part.manufacturer_name)
 | 
			
		||||
 | 
			
		||||
        # Skip empty names
 | 
			
		||||
        if not name or len(name) == 0:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # Can be linked to an existing manufacturer
 | 
			
		||||
        if link_part(part, name):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # Find a list of potential matches
 | 
			
		||||
        matches = find_matches(name)
 | 
			
		||||
 | 
			
		||||
        clear()
 | 
			
		||||
 | 
			
		||||
        # Present a list of options
 | 
			
		||||
        print("----------------------------------")
 | 
			
		||||
        print("Checking part {idx} of {total}".format(idx=idx+1, total=total))
 | 
			
		||||
        print("Manufacturer name: '{n}'".format(n=name))
 | 
			
		||||
        print("----------------------------------")
 | 
			
		||||
        print("Select an option from the list below:")
 | 
			
		||||
 | 
			
		||||
        print("0) - Create new manufacturer '{n}'".format(n=name))
 | 
			
		||||
        print("")
 | 
			
		||||
 | 
			
		||||
        for i, m in enumerate(matches[:10]):
 | 
			
		||||
            print("{i}) - Use manufacturer '{opt}'".format(i=i+1, opt=m))
 | 
			
		||||
 | 
			
		||||
        print("")
 | 
			
		||||
        print("OR - Type a new custom manufacturer name")
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
        while (1):
 | 
			
		||||
            response = str(input("> ")).strip()
 | 
			
		||||
 | 
			
		||||
            # Attempt to parse user response as an integer
 | 
			
		||||
            try:
 | 
			
		||||
                n = int(response)
 | 
			
		||||
 | 
			
		||||
                # Option 0) is to create a new manufacturer with the current name
 | 
			
		||||
                if n == 0:
 | 
			
		||||
 | 
			
		||||
                    create_manufacturer(part, name, name)
 | 
			
		||||
                    return
 | 
			
		||||
 | 
			
		||||
                # Options 1) -> n) select an existing manufacturer
 | 
			
		||||
                else:
 | 
			
		||||
                    n = n - 1
 | 
			
		||||
 | 
			
		||||
                    if n < len(matches):
 | 
			
		||||
                        # Get the company which matches the selected options
 | 
			
		||||
                        company_name = matches[n]
 | 
			
		||||
                        company = companies[company_name]
 | 
			
		||||
 | 
			
		||||
                        # Ensure the company is designated as a manufacturer
 | 
			
		||||
                        company.is_manufacturer = True
 | 
			
		||||
                        company.save()
 | 
			
		||||
 | 
			
		||||
                        # Link the company to the part
 | 
			
		||||
                        part.manufacturer = company
 | 
			
		||||
                        part.save()
 | 
			
		||||
 | 
			
		||||
                        # Link the name to the company
 | 
			
		||||
                        links[name] = company
 | 
			
		||||
                        links[company_name] = company
 | 
			
		||||
 | 
			
		||||
                        print(" -> Linked '{n}' to manufacturer '{m}'".format(n=name, m=company_name))
 | 
			
		||||
 | 
			
		||||
                        return
 | 
			
		||||
 | 
			
		||||
            except ValueError:
 | 
			
		||||
                # User has typed in a custom name!
 | 
			
		||||
 | 
			
		||||
                if not response or len(response) == 0:
 | 
			
		||||
                    # Response cannot be empty!
 | 
			
		||||
                    print("Please select an option")
 | 
			
		||||
                
 | 
			
		||||
                # Double-check if the typed name corresponds to an existing item
 | 
			
		||||
                elif response in companies.keys():
 | 
			
		||||
                    link_part(part, companies[response])
 | 
			
		||||
                    return
 | 
			
		||||
 | 
			
		||||
                elif response in links.keys():
 | 
			
		||||
                    link_part(part, links[response])
 | 
			
		||||
                    return
 | 
			
		||||
 | 
			
		||||
                # No match, create a new manufacturer
 | 
			
		||||
                else:
 | 
			
		||||
                    create_manufacturer(part, name, response)
 | 
			
		||||
                    return
 | 
			
		||||
 | 
			
		||||
    clear()
 | 
			
		||||
    print("")
 | 
			
		||||
    clear()
 | 
			
		||||
 | 
			
		||||
    print("---------------------------------------")
 | 
			
		||||
    print("The SupplierPart model needs to be migrated,")
 | 
			
		||||
    print("as the new 'manufacturer' field maps to a 'Company' reference.")
 | 
			
		||||
    print("The existing 'manufacturer_name' field will be used to match")
 | 
			
		||||
    print("against possible companies.")
 | 
			
		||||
    print("This process requires user input.")
 | 
			
		||||
    print("")
 | 
			
		||||
    print("To cancel this mapping process, press Ctrl-C in the terminal.")
 | 
			
		||||
    print("Note: This process MUST be completed to migrate the database.")
 | 
			
		||||
    print("---------------------------------------")
 | 
			
		||||
    print("")
 | 
			
		||||
 | 
			
		||||
    input("Press <ENTER> to continue.")
 | 
			
		||||
 | 
			
		||||
    clear()
 | 
			
		||||
 | 
			
		||||
    part_count = SupplierPart.objects.count()
 | 
			
		||||
 | 
			
		||||
    # Create a unique set of manufacturer names
 | 
			
		||||
    for idx, part in enumerate(SupplierPart.objects.all()):
 | 
			
		||||
 | 
			
		||||
        if part.manufacturer is not None:
 | 
			
		||||
            print(" -> Part '{p}' already has a manufacturer associated (skipping)".format(p=part))
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        map_part_to_manufacturer(part, idx, part_count)
 | 
			
		||||
        parts.append(part)
 | 
			
		||||
 | 
			
		||||
    print("Done!")
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('company', '0018_supplierpart_manufacturer'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RunPython(associate_manufacturers, reverse_code=reverse_association)
 | 
			
		||||
    ]
 | 
			
		||||
		Reference in New Issue
	
	Block a user