Skip to content

Instantly share code, notes, and snippets.

@jerinisready
Last active June 29, 2019 05:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jerinisready/79f54867f5ceb084228b62d89378e877 to your computer and use it in GitHub Desktop.
Save jerinisready/79f54867f5ceb084228b62d89378e877 to your computer and use it in GitHub Desktop.
How to Create A Friendly Unique id such as an automatically generated Invoice ID.
Friendly ID / Auto Generate Invoice ID on First Save.

This is just modified version of friendly id for make this script compatible with python 3.x

Invoice numbers like "0000004" are a little unprofessional in that they expose how many sales a system has made, and can be used to monitor the rate of sales over a given time. They are also harder for customers to read back to you, especially if they are 10 digits long.

This is simply a perfect hash function to convert an integer (from eg an ID AutoField) to a unique number. The ID is then made shorter and more user-friendly by converting to a string of letters and numbers that wont be confused for one another (in speech or text).

To use it:
import friendly_id

class MyModel(models.Model): invoice_id = models.CharField(max_length=6, null=True, blank=True, unique=True)

def save(self, *args, **kwargs):
  super(MyModel, self).save(*args, **kwargs)

  # Populate the invoice_id if it is missing
  if self.id and not self.invoice_id:
    self.invoice_id = friendly_id.encode(self.id)
    self.save()
Explanation

if self.id and not self.invoice_id

When an object from this model is saved, an invoice ID will be generated that does not resemble those surrounding it. For example, where you are expecting millions of invoices the IDs generated from the AutoField primary key will be:

obj.id   obj.invoice_id
  1        TTH9R
  2        45FLU
  3        6ACXD
  4        8G98W
  5        AQ6HF
  6        DV3TY
  ...
  9999999  J8UE5

The functions are deterministic, so running it again sometime will give the same result, and generated strings are unique for the given range (the default max is 10,000,000). Specifying a higher range allows you to have more IDs, but all the strings will then be longer. You have to decide which you need: short strings or many strings :-)

This problem could have also been solved using a random invoice_id generator, but that might cause collisions which cost time to rectify, especially when a decent proportion of the available values are taken (eg 10%). Anyhow, someone else has now already written this little module for you, so now you don't have to write your own :-)

# Credits: [Rana ](https://djangosnippets.org/users/rana-ahmed/)
# More Documented Snippet will be available at (https://djangosnippets.org/snippets/10613/)
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
""" Generates and decodes an unique invoice id, which can use characters to shorten its length.
Author: Will Hardy Modified by: Minuddin Ahmed Rana
Usage: >>> encode(1)
"488KR"
Description: Invoice numbers like "0000004" are unprofessional in that they expose how many sales a system has made, and can be used to monitor
the rate of sales over a given time. They are also harder for customers to read back to you, especially if they are 10 digits long.
These functions convert an integer (from eg an ID AutoField) to a short unique string. This is done simply using a perfect hash
function and converting the result into a string of user friendly characters.
"""
import math
from itertools import chain
import warnings
try:
from django.conf import settings
getattr(settings, 'DEBUG', None)
except ImportError:
settings = None
# Keep this small for shorter strings, but big enough to avoid changing it later. If you do change it later, it might be a good idea to specify a STRING_LENGTH change, making all future strings longer, and therefore unique.
SIZE = getattr(settings, 'FRIENDLY_ID_SIZE', 10000000)
# OPTIONAL PARAMETERS
OFFSET = getattr(settings, 'FRIENDLY_ID_OFFSET', SIZE / 2 - 1)
VALID_CHARS = getattr(settings, 'FRIENDLY_ID_VALID_CHARS', "3456789ACDEFGHJKLQRSTUVWXY") # Alpha numeric characters, only uppercase, no confusing values (eg 1/I,0/O,Z/2)
PERIOD = getattr(settings, 'FRIENDLY_ID_PERIOD', None) # Don't set this, it isn't necessary and you'll get ugly strings like 'AAAAAB3D'
STRING_LENGTH = getattr(settings, 'FRIENDLY_ID_STRING_LENGTH', None) # Don't set this, it isn't necessary and you'll get ugly strings like 'AAAAAB3D'
def find_suitable_period():
""" Automatically find a suitable period to use. Factors are best, because they will have 1 left over when dividing SIZE+1. This only needs to be run once, on import. """
# The highest acceptable factor will be the square root of the size.
highest_acceptable_factor = int(math.sqrt(SIZE))
starting_point = len(VALID_CHARS) > 14 and len(VALID_CHARS)/2 or 13
for p in list(chain(range(int(starting_point), 7, -1), range(highest_acceptable_factor, int(starting_point)+1, -1))) + [6, 5, 4, 3, 2]:
if SIZE % p == 0:
return p
raise Exception("No valid period could be found for SIZE=%d. Try avoiding prime numbers :-)" % SIZE)
# Set the period if it is missing
if not PERIOD:
PERIOD = find_suitable_period()
def perfect_hash(num):
return ((num+OFFSET)*(SIZE/PERIOD)) % (SIZE+1) + 1
def friendly_number(num):
string = ""
while STRING_LENGTH and len(string) <= STRING_LENGTH or len(VALID_CHARS)**len(string) <= SIZE:
string = VALID_CHARS[int(num % len(VALID_CHARS))] + string
num = num/len(VALID_CHARS)
return string
def encode(num):
# Check the number is within our working range
if not 0 < num < SIZE: return None
return friendly_number(perfect_hash(num))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment