{"id":73,"date":"2008-07-01T16:58:50","date_gmt":"2008-07-01T21:58:50","guid":{"rendered":"http:\/\/blogs.cae.tntech.edu\/mwr\/2008\/07\/01\/better-inventory-management-through-python\/"},"modified":"2024-10-27T14:26:19","modified_gmt":"2024-10-27T14:26:19","slug":"better-inventory-management-through-python","status":"publish","type":"post","link":"https:\/\/sites.tntech.edu\/renfro\/2008\/07\/01\/better-inventory-management-through-python\/","title":{"rendered":"Better Inventory Management Through Python"},"content":{"rendered":"<p>One good thing about the switch from the legacy administrative computing system to the new Banner setup is now I can more easily get an unformatted table of my department&#8217;s equipment inventory instead of a printed report. This matters to me for a few reasons:<\/p>\n<ul>\n<li>The default inventory reports always come out sorted by inventory tag. I have no idea why. If you have anything more than a handful of items to inventory, you would sort them by room, just like you&#8217;d identify them when walking around. Since I have somewhere around 100 items to inventory across 6-7 buildings, it&#8217;s a pain and rather error-prone to manually mark the ones in a particular building.<\/li>\n<li>Even when I got a report sorted by location, that doesn&#8217;t help a great deal with items that I only see during inventory time, or during a random audit. My memory just isn&#8217;t that good, and occasionally an item will move from one room to another without my knowledge. Photos of the items would help greatly in finding them, but the default reports don&#8217;t include any.<\/li>\n<\/ul>\n<p>So this year, within a few hours of first asking for it, I got a nice CSV file of our inventory from the Business Office (thanks, Matt). It included lines like<br \/>\n<code><br \/>\n74396,\"Fork Lift, Electric\",SU 803647,F30,CATERPILLAR,5\/1\/2001,CH106,13900,Center for Manufacturing Research,6700<br \/>\n<\/code><br \/>\nthat I could work with. I had text files from previous years on the old system, but they were much weirder to parse. This opened right up in Excel without issue, so I had hope for a much cleaner implementation of my old Python inventory scripts.<\/p>\n<p>Man, I love Python. There&#8217;s <a href=\"http:\/\/docs.python.org\/lib\/module-csv.html\">a built-in CSV module<\/a> that parses lines into lists, and separates values on each line into separate items in those lists. No parsing required on my part. There&#8217;s a nice third-party module for generating HTML markup (original site is down, but see <a href=\"http:\/\/web.archive.org\/web\/20070409120933\/http:\/\/dustman.net\/andy\/python\/HyperText\/1.0.1\/HyperText-1.0.1.tar.gz\">this link from archive.org<\/a> for the module). And it&#8217;s a mature enough language to where <a href=\"http:\/\/groups.google.com\/group\/comp.lang.python\/msg\/42b5a84450278ab2\">a years-old Usenet post on sorting<\/a> is still useful.<\/p>\n<p>On a procedural rather than technical front, I started going around a couple of years ago and snapping pictures of items as I inventoried them. The old camera-phone pictures aren&#8217;t great, but they&#8217;re good enough to identify an item and find its property tag. So now I had a table of inventory data, and a big folder of JPGs named according to the item&#8217;s property tag. So now to convert them into useful web pages:<br \/>\n<!--more--><\/p>\n<pre>\nimport csv\nimport os\nimport string\nfrom HyperText.XHTML import TABLE, TR, TH, TD, IMG, DIV, HEAD, STYLE, BR\nfrom HyperText.Documents import Document\n\nnotesDict = {\n    '064286':'(2007) Surplused',\n    }\n\n\n\"\"\"A set of comparer classes.\"\"\"\n## http:\/\/groups.google.com\/group\/comp.lang.python\/msg\/42b5a84450278ab2\n\nclass CmpComposite:\n    \"\"\"Takes a list of compare functions and sorts in that order.\"\"\"\n    def __init__(self,*comparers):\n        self.comparers=comparers\n    def __call__(self,a,b):\n        for cmp in self.comparers:\n            c=cmp(a,b)\n            if c:\n                return c\n        return 0\n\nclass CmpInverse:\n    \"\"\"Inverses the effect of a cmp.\"\"\"\n    def __init__(self,cmp):\n        self.cmp=cmp\n    def __call__(self,a,b):\n        return -self.cmp(a,b)\n\nclass CmpColumn:\n    \"\"\"Sorts on an index of a sequence.\"\"\"\n    def __init__(self,column):\n        self.column=column\n    def __call__(self,a,b):\n        return cmp(a[self.column],b[self.column])\n    \nclass CmpAttr:\n    \"\"\"Sorts on an attribute.\"\"\"\n    def __init__(self, attr):\n        self.attr = attr\n    def __call__(self, x, y):\n        return cmp(getattr(x, self.attr), getattr(y, self.attr))\n\nreader=csv.reader(open('inventory_control_listing_13900.csv','rb'))\nequipmentList = []\nfor row in reader:\n    (tag, description, serial, model, manufacturer, arrival,\n     location, code, department, cost) = row\n    equipmentList.append((tag, description, serial, model, manufacturer,\n                          arrival, location, cost))\n\nfor sortBy in ['tag', 'location']:\n    if sortBy=='tag':\n        equipmentList.sort(CmpColumn(0))\n    elif sortBy=='location':\n        equipmentList.sort(CmpColumn(6))\n\n    outFilename = \"by-%s.html\" % (sortBy)\n    print\n    print \"Would print to %s\" % (outFile)\n    print\n    \n    n=0\n    seenTags = []\n    inventoryTableList=DIV()\n    for equipmentItem in equipmentList:\n        (itemTag, itemDescription, itemSerial, itemModel, itemManufacturer,\n         itemArrival, itemLocation, itemCost) = equipmentItem\n\n        try:\n            seenTags.index(itemTag)\n        except ValueError:\n            seenTags.append(itemTag)\n            n=n+1\n            if (n%2)==0:\n                inventoryTable=TABLE(border='1',style='page-break-after: always')\n            else:\n                inventoryTable=TABLE(border='1')\n\n            headerRow=TR()\n            headerRow.append(TH('Tag'))\n            headerRow.append(TH('Item Description'))\n            headerRow.append(TH('Serial Number'))\n            headerRow.append(TH('Model Number'))\n            headerRow.append(TH('Arrival'))\n            headerRow.append(TH('Manufacturer'))\n            headerRow.append(TH('Location'))\n            headerRow.append(TH('Cost'))\n            inventoryTable.append(headerRow)\n\n            itemRow=TR()\n            if itemTag[0] in string.digits:\n                itemTag=''.join(['0',itemTag])\n            itemRow.append(TD(itemTag,align='right'))\n            itemRow.append(TD(itemDescription))\n            itemRow.append(TD(itemSerial))\n            itemRow.append(TD(itemModel))\n            itemRow.append(TD(itemArrival,align='right'))\n            itemRow.append(TD(itemManufacturer))\n            itemRow.append(TD(itemLocation,align='center'))\n            itemRow.append(TD(\"$%.2f\" % (float(itemCost)),align='right'))\n            inventoryTable.append(itemRow)\n\n            imageRow=TR()\n            if os.path.exists(\"%s.jpg\" % itemTag):\n                imageRow.append(TD(IMG(src=\"%s.jpg\" % itemTag),\n                                   colspan=4,align='center',width='640'))\n            else:\n                imageRow.append(TD(IMG(src=\"missing.gif\"),\n                                   colspan=4,align='center',width='640'))\n\n            if os.path.exists(\"%s-detail.jpg\" % itemTag):\n                imageRow.append(TD(IMG(src=\"%s-detail.jpg\" % itemTag),\n                                   colspan=4,align='center',width='640'))\n            else:\n                imageRow.append(TD(IMG(src=\"missing.gif\"),\n                                   colspan=4,align='center',width='640'))\n\n            inventoryTable.append(imageRow)\n\n            notesRow=TR()\n            if notesDict.has_key(itemTag):\n                notesRow.append(TD(\"Notes: %s\" % notesDict[itemTag],colspan=8))\n            else:\n                notesRow.append(TD(\"Notes:\",colspan=8))\n\n            inventoryTable.append(notesRow)\n\n            inventoryTableList.append(inventoryTable)\n            inventoryTableList.append(BR())\n\n    head=HEAD()\n    head.append(STYLE(type='text\/css'))\n    htmlOutput=Document(head,inventoryTableList)\n    htmlFile=open(outFilename,'w+')\n    htmlOutput.writeto(htmlFile)\n    htmlFile.close()\n<\/pre>\n<p>Once this script has run, I have two web pages of the same inventory data: one sorted by location, and another by property tag. I generally only use the location-sorted one, though. The CSS tries to force page breaks every two items, and judicious use of landscape printing and a bit of scaling in Firefox means I can carry around 40 pages of my report and find everything I need pretty quickly.<br \/>\n<img src='http:\/\/sites.tntech.edu\/renfro\/wp-content\/uploads\/sites\/111\/2008\/07\/inventory-screenshot.png' alt='Screenshot of Inventory Report in Firefox' \/><\/p>\n","protected":false},"excerpt":{"rendered":"<p>One good thing about the switch from the legacy administrative computing system to the new Banner setup is now I can more easily get an unformatted table of my department&#8217;s equipment inventory instead of a printed report. This matters to me for a few reasons: The default inventory reports always come out sorted by inventory &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/sites.tntech.edu\/renfro\/2008\/07\/01\/better-inventory-management-through-python\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;Better Inventory Management Through Python&#8221;<\/span><\/a><\/p>\n","protected":false},"author":87,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[17],"tags":[],"class_list":["post-73","post","type-post","status-publish","format-standard","hentry","category-python","entry"],"_links":{"self":[{"href":"https:\/\/sites.tntech.edu\/renfro\/wp-json\/wp\/v2\/posts\/73","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sites.tntech.edu\/renfro\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sites.tntech.edu\/renfro\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sites.tntech.edu\/renfro\/wp-json\/wp\/v2\/users\/87"}],"replies":[{"embeddable":true,"href":"https:\/\/sites.tntech.edu\/renfro\/wp-json\/wp\/v2\/comments?post=73"}],"version-history":[{"count":1,"href":"https:\/\/sites.tntech.edu\/renfro\/wp-json\/wp\/v2\/posts\/73\/revisions"}],"predecessor-version":[{"id":463,"href":"https:\/\/sites.tntech.edu\/renfro\/wp-json\/wp\/v2\/posts\/73\/revisions\/463"}],"wp:attachment":[{"href":"https:\/\/sites.tntech.edu\/renfro\/wp-json\/wp\/v2\/media?parent=73"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sites.tntech.edu\/renfro\/wp-json\/wp\/v2\/categories?post=73"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sites.tntech.edu\/renfro\/wp-json\/wp\/v2\/tags?post=73"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}