mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Custom migration for walking user through the process of mapping supplierpart to manufacturer
This commit is contained in:
parent
890e938662
commit
290002fe9d
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)
|
||||
]
|
Loading…
Reference in New Issue
Block a user