Teach stock keeper some new tricks

trunk
alexis 2021-08-30 22:19:59 -06:00
parent c195784ab7
commit 7932570e50
2 changed files with 370 additions and 166 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ __pycache__
*.FCStd1
*.vcd
*.elf
apikey_digikey.py
# Ignore list for: kicad
*.xml

View File

@ -1,13 +1,82 @@
#!/usr/bin/python3
import csv
import copy
import os
import json
from colorama import Fore, Style
try:
import apikey_digikey
except ImportError:
print("DigiKey lookups will not be available without apikey_digikey.py")
have_digikey = False
else:
import digikey
import requests
import urllib
have_digikey = True
# Control codes to scan GS as Tab on the Tera piece of crap (encode as any
# common format, scan in sequence):
#
# %%SpecCodeEE
# %%SpecCodeEF
# %%09
#
#
# Also, the Shut The Fuck Up command is: %%SpecCode94
# And the Do Not Go To Sleep command is: %%SpecCode36
PARTSDB_FN = "partsdb.csv"
LOCDB_FN = "locdb.csv"
class DkBarcodeInfo:
def __init__(self, bc):
self.sandbox = apikey_digikey.SANDBOX
self._digikeyApiToken = digikey.oauth.oauth2.TokenHandler(version=3, sandbox=self.sandbox).get_access_token()
self.authorization = self._digikeyApiToken.get_authorization()
bc = bc.replace("\t", "\x1d")
url = ("https://api.digikey.com/Barcoding/v3/Product2DBarcodes/"
+ urllib.parse.quote(bc).replace("/", "%2F")
)
headers = {
"Authorization": self.authorization.replace("BearerToken", "Bearer"),
"authorization": self.authorization.replace("BearerToken", "Bearer"),
"accept": "application/json",
"X-DIGIKEY-Client-Id": os.environ["DIGIKEY_CLIENT_ID"],
}
rsp = requests.get(
url,
headers = headers,
)
data = rsp.json()
with open("/tmp/stockist-dkbcdump", "a") as f:
json.dump(data, f, indent=True)
self.dkpn = data["DigiKeyPartNumber"]
self.mpn = data["ManufacturerPartNumber"]
self.manuf = data["ManufacturerName"]
self.descr = data["ProductDescription"]
self.qty = data["Quantity"]
self.salesorder = data["SalesorderId"]
def is_dk(bc):
"""Check if a barcode is probably DigiKey"""
return (not bc.startswith("avl")) and ("\t" in bc or "\x1D" in bc)
def is_barcode(bc):
"""Check if something returned from _scan_barcode() is a barcode (else
a stock ID)"""
return bc.startswith("avl") or bc.startswith("DK(")
def query(prompt, default, parser):
while True:
resp = input(prompt + " ").strip()
resp = input(Fore.YELLOW + prompt + Style.RESET_ALL + " ").strip()
if not resp:
resp = default
@ -26,6 +95,8 @@ class Stockist:
self.parts = []
self.locs = []
self.loc = ""
self.explained_digikey = False
self.last_choice = None
def load(self):
print("Loading database")
@ -76,43 +147,48 @@ class Stockist:
writer.writerow(fields)
def step(self):
print()
print("0. Exit")
print("1. Consume")
print("2. Intake")
print("3. Add new")
print("4. Edit")
print("5. Move")
print("6. Drop")
print("7. Add barcode")
print("10. Set location")
print("11. Register location")
print("12. Where is?")
if self.last_choice is None:
self._show_head("MENU")
print("0. Exit")
print("1. Consume")
print("2. Intake")
print("3. Edit")
print("4. Move")
print("5. Drop")
print("6. Add or change barcode")
print("9. Look up")
print("10. Set location")
print("11. Register location")
choice = query("Choice?", -1, int)
choice = query("Choice?", -1, int)
else:
choice = self.last_choice
if choice == 0:
return False
elif choice == 1:
self.consume()
done = self.consume()
elif choice == 2:
self.intake()
done = self.intake()
elif choice == 3:
self.add()
done = self.edit()
elif choice == 4:
self.edit()
done = self.move()
elif choice == 5:
self.move()
done = self.drop()
elif choice == 6:
self.drop()
elif choice == 7:
self.add_barcode()
done = self.change_barcode()
elif choice == 9:
done = self.lookup()
elif choice == 10:
self.set_location()
done = self.set_location()
elif choice == 11:
self.reg_location()
elif choice == 12:
self.where_is()
done = self.reg_location()
if not done:
self.last_choice = choice
else:
self.last_choice = None
return True
@ -141,126 +217,138 @@ class Stockist:
return None
def consume(self):
while True:
barcode = query("Scan:", "", str)
if not barcode:
return True
self._show_head("CONSUME")
part = self.findone("Barcode", barcode)
if part is None:
continue
barcode, _ = self._scan_barcode()
if not barcode:
return True
consumed = query("How many used?", 0, int)
part["Stock"] -= consumed
self.save()
part = self.findone("Barcode", barcode)
if part is None:
return
print(f"New quantity: {part['Stock']}")
print()
print("Currently in stock", part["Stock"])
consumed = query("How many used?", 0, int)
part["Stock"] -= consumed
self.save()
print(f"New quantity: {part['Stock']}")
print()
def intake(self):
while True:
barcode = query("Scan:", "", str)
if not barcode:
return True
self._show_head("INTAKE")
part = self.findone("Barcode", barcode)
barcode, binfo = self._scan_barcode()
if not barcode:
return True
if part is None:
self.add(barcode_scanned=barcode)
return True
part = self.findone("Barcode", barcode)
added = query("How many added?", 0, int)
part["Stock"] += added
self.save()
if part is None:
self.add(barcode_scanned=(barcode, binfo))
return
print(f"New quantity: {part['Stock']}")
print()
print("Currently in stock", part["Stock"])
added = query("How many added?", 0, int)
part["Stock"] += added
self.save()
print(f"New quantity: {part['Stock']}")
print()
def add(self, barcode_scanned=None):
while True:
if barcode_scanned is None:
barcode = query("Scan:", "", str)
if not barcode:
return True
else:
barcode = barcode_scanned
if self.findone("Barcode", barcode, silent=True) is not None:
edit = query("Already in database! Edit?", "n", yes)
if edit:
self.edit(barcode_scanned = barcode)
return True
part = {"Barcode": barcode}
part["Loc"] = query(f"Location ({self.loc})?", self.loc, str)
part["Name"] = query("Stock ID?", "", str)
if part["Name"] == "":
print("Required!")
continue
part["Stock"] = query("In stock (0)?", 0, int)
part["Restock"] = query("Num to restock (0)?", 0, int)
part["Pkg"] = query("Num per package (1)?", 1, int)
part["MFR"] = query("Manuf?", "", str)
part["MPN"] = query("MPN?", "", str)
part["Dist"] = query("Dist (DK)?", "DK", str)
part["DPN"] = query("Dist PN?", "", str)
part["Price"] = query("Price?", "", str)
part["Notes"] = query("Notes?", "", str)
print()
accept = query("Accept?", "n", yes)
if accept:
self.parts.append(part)
self.save()
if barcode_scanned is not None:
# If we were given a barcode, just do that one
if barcode_scanned is None:
barcode, binfo = self._scan_barcode()
if not barcode:
return True
else:
barcode, binfo = barcode_scanned
if self.findone("Barcode", barcode, silent=True) is not None:
edit = query("Already in database! Edit?", "n", yes)
if edit:
self.edit(barcode_scanned=(barcode, binfo))
return
part = {"Barcode": barcode}
self._populate_part(part, barcode, binfo)
print()
accept = query("Accept?", "n", yes)
if accept:
self.parts.append(part)
self.save()
if barcode_scanned is not None:
# If we were given a barcode, just do that one
return
def edit(self, barcode_scanned=None):
while True:
if barcode_scanned is None:
barcode = query("Scan:", "", str)
if not barcode:
return True
else:
barcode = barcode_scanned
self._show_head("EDIT")
part = self.findone("Barcode", barcode)
if part is None:
if barcode_scanned is None:
barcode, binfo = self._scan_barcode()
if not barcode:
return True
else:
barcode, binfo = barcode_scanned
upd = copy.copy(part)
part = self.findone("Barcode", barcode)
fields = [
("Barcode", "Barcode", str),
("Loc", "Location", str),
("Name", "Name", str),
("Stock", "In stock", int),
("Restock", "Num to restock", int),
("Pkg", "Num in package", int),
("MFR", "MFR", str),
("MPN", "MPN", str),
("Dist", "Distributor", str),
("DPN", "Distrib PN", str),
("Price", "Price", str),
("Notes", "Notes", str),
]
for field, descr, ty in fields:
upd[field] = query(f"{descr} ({part[field]})?", part[field], ty)
if part is None:
print("NOT FOUND")
return
print()
accept = query("Accept?", "n", yes)
if accept:
part.update(upd)
self.save()
upd = copy.copy(part)
_populate_part(upd)
if barcode_scanned is not None:
# If we were given a barcode, just do that one
return True
print()
accept = query("Accept?", "n", yes)
if accept:
part.update(upd)
self.save()
def lookup(self):
self._show_head("LOOKUP")
barcode, binfo = self._scan_barcode(accept_stock_id=True)
if barcode is None:
return True
key = "Barcode" if is_barcode(barcode) else "Name"
for loc in self.locs:
if loc["Barcode"] == barcode:
print("LOCATION:", loc["Descr"])
return
matches = [i for i in self.parts if i[key] == barcode]
width = 0
for i in matches:
for k, v in i.items():
width = max(width, len(k))
for i in matches:
print("STOCK ID:", i["Name"])
for k, v in i.items():
if k == "Name":
continue
if k == "Loc":
for loc in self.locs:
if loc["Barcode"] == i["Loc"]:
v += " (" + loc["Descr"] + ")"
break
print(f" {k:<{width}} = {v}")
if not matches:
print("NOT FOUND")
def move(self):
self._show_head("MOVE")
while True:
barcode = query("Scan:", "", str)
if not barcode:
@ -273,6 +361,8 @@ class Stockist:
self.save()
def drop(self):
self._show_head("DROP")
while True:
barcode = query("Scan:", "", str)
if not barcode:
@ -293,41 +383,52 @@ class Stockist:
del self.parts[index]
self.save()
def add_barcode(self):
while True:
name = query("Stock ID?", "", str)
if not name:
return True
def change_barcode(self):
self._show_head("ADD OR CHANGE BARCODE")
part = self.findone("Name", name)
if part is None:
continue
barcode, binfo = self._scan_barcode(accept_stock_id=True)
part["Barcode"] = query("Scan barcode:", "", str)
if barcode is None:
return True
if self.loc:
key = "Barcode" if is_barcode(barcode) else "Name"
part = self.findone(key, barcode)
if part is None:
return
barcode, binfo = self._scan_barcode()
part["Barcode"] = barcode
if self.loc:
if part.get("Loc", self.loc) != self.loc:
if query("Change location?", "n", yes):
part["Loc"] = self.loc
else:
part["Loc"] = self.loc
print("Added location:", self.loc)
else:
print("No location set, did not change")
else:
print("No location set, did not change")
self.save()
self.save()
def set_location(self):
self._show_head("SET CURRENT LOCATION")
loc = query("Location?", "", str)
self.loc = loc
return True
def reg_location(self):
while True:
barcode = query("Scan:", "", str)
if not barcode:
return True
self._show_head("REGISTER LOCATION")
if any(i.get("Barcode", "") == barcode for i in self.parts):
print("Barcode already registered as a part!")
continue
else:
break
barcode = query("Scan:", "", str)
if not barcode:
return True
if any(i.get("Barcode", "") == barcode for i in self.parts):
print("Barcode already registered as a part!")
return
if any(i.get("Barcode", "") == barcode for i in self.locs):
print("Re-registering!")
@ -338,30 +439,132 @@ class Stockist:
self.locs.append({"Barcode": barcode, "Descr": descr})
self.save()
def where_is(self):
while True:
name_or_barcode = query("Stock ID or barcode?", "", str)
if query("Set to this location and return to menu?", "n", yes):
self.loc = barcode
return True
if not name_or_barcode:
return True
if name_or_barcode.startswith("avl"):
part = self.findone("Barcode", name_or_barcode)
else:
part = self.findone("Name", name_or_barcode)
def _scan_barcode(self, accept_stock_id=False):
"""Scan a barcode. Can accept DigiKey barcodes.
if part is None:
accept_stock_id: also accept stock IDs. They'll be returned as
`barcode`
Return barcode, binfo
barcode is the barcode value (if a DK barcode, this is DK(partnumber))
binfo is a DkBarcodeInfo if DK barcode, else None
"""
if have_digikey and not self.explained_digikey:
print("DigiKey API support is enabled. You can scan a DigiKey "
"barcode at any time (except as location)")
print("to fill in fields from that. If using a custom stock asset "
"label, you should still scan that.")
print("If this barcode scanner cannot enter the GS byte, it must "
"be configured to substitute a Horizontal Tab.")
print("For my piece of crap scanner, the control codes to do this "
"can be found at the top of this file.")
print()
self.explained_digikey = True
binfo = None
barcode = query(
"Barcode or stock ID:" if accept_stock_id else "Barcode:",
"", str
)
if not barcode:
return None, None
if have_digikey and is_dk(barcode):
binfo = DkBarcodeInfo(barcode)
barcode = "DK(" + binfo.dkpn + ")"
return barcode, binfo
else:
return barcode, None
def _populate_part(self, part, barcode=None, binfo=None):
"""Populate the fields of a part. If it already contains values, the
user will be prompted to edit them.
Return False if the user scanned an empty barcode (canceled), otherwise
returns True"""
if barcode is None:
barcode, binfo = self._scan_barcode()
if barcode is None:
return False
loc = part.get("Loc", self.loc)
loc = query(f"Location ({loc})?", loc, str)
part["Loc"] = loc
fields = [
("Name", "Name", str, ""),
("Stock", "In stock", int, 0),
("Restock", "Num to restock", int, 0),
("Pkg", "Num in package", int, 1),
("Decsr", "Description", str, ""),
("MFR", "MFR", str, ""),
("MPN", "MPN", str, ""),
("Dist", "Distributor", str, ""),
("DPN", "Distrib PN", str, ""),
("Price", "Price", str, ""),
("Notes", "Notes", str, ""),
]
n = 0
while n < len(fields):
field, descr, ty, dflt = fields[n]
n += 1
if binfo is not None:
break
orig = part.get(field, dflt)
scan = query(f"{descr} ({orig})?", part.get(field, dflt), str)
if have_digikey and is_dk(barcode):
binfo = DkBarcodeInfo(barcode)
break
try:
value = ty(scan)
except (ValueError, TypeError):
n -= 1
continue
if not part["Loc"]:
print("Part has no location set")
else:
for i in self.locs:
if i["Barcode"] == part["Loc"]:
print("Location:", i["Barcode"], "=", i["Descr"])
break
else:
print("Part says it's in", part["Loc"], "but we don't "
"recognize that location")
part[field] = value
if binfo is not None:
consumed = query("How many have been used (0)?", 0, int)
part["Stock"] = binfo.qty - consumed
part["Restock"] = query("How many to restock (0)?", 0, int)
part["Pkg"] = query("How many in package (1)?", 1, int)
part["Name"] = query("fStock ID (DK({binfo.dkpn}))?",
binfo.dkpn, str)
part["MFR"] = binfo.manuf
part["MPN"] = binfo.mpn
part["Dist"] = "DK"
part["DPN"] = binfo.dkpn
part["Price"] = "" # TODO
part["Descr"] = binfo.descr
print( "Loaded from DigiKey:")
print(f" Stock = {binfo.qty} (remaining: {part['Stock']})")
print(f" MFR = {binfo.manuf}")
print(f" MPN = {binfo.mpn}")
print(f" DPN = {binfo.dkpn}")
print(f" Descr = {binfo.descr}")
return True
def _show_head(self, title):
print()
print(Fore.GREEN + "==", title, "==" + Style.RESET_ALL, end='')
if self.loc:
loc_name = [i for i in self.locs if i["Barcode"] == self.loc][0]["Descr"]
print(Fore.CYAN + f" (in {self.loc} / {loc_name})" + Style.RESET_ALL,
end='')
print()
if __name__ == "__main__":
s = Stockist()