Teach stock keeper some new tricks
parent
c195784ab7
commit
7932570e50
|
@ -11,6 +11,7 @@ __pycache__
|
|||
*.FCStd1
|
||||
*.vcd
|
||||
*.elf
|
||||
apikey_digikey.py
|
||||
|
||||
# Ignore list for: kicad
|
||||
*.xml
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue