inventory_mgt/
lib.rs

1//! # Inventory Management
2//! 
3//! This crate is a port of an existing python project. It allows one to sync the current
4//! quantities from a supply inventory csv file to a filtered down view they have created of their
5//! own truncated "master inventory" (their own inventory)
6//! 
7//! ### This program will update the quantity in master inventory with the quantity found in supply inventory based for each part in master inventory
8//! 
9//! 
10//! - The default filenames that program accepts are:  
11//!    - "SupplyInventory.csv" for the _supply list_  
12//!    - "MasterInventory.csv" for the _master list_   
13//! - _You can set your own filenames but you must specify their name and location in command line arguments_
14//!    - _See how to use command line arguments below_
15//! 
16//! 
17//! - Both csv files must include the following columns:  
18//! 
19//!    | VenCode | PartNumber | TotalQty |  
20//!    | ------- |:----------:|:--------:|  
21//! 
22//! ## Installation
23//!
24//! You can install this application in one of two ways.  Either clone the repo and build the
25//! release version with cargo, or simply use cargo install:
26//!
27//! ```terminal
28//! $ cargo install inventory-mgt
29//! ```
30//! 
31//! ## Use
32//!
33//! To use this application, simply run it with the generate command to generate a new master csv
34//! with updated quantity fields:
35//!
36//! ```terminal
37//! $ inventory_mgt generate
38//! ```
39//!
40//! Make sure you are in the root folder where your `SupplyInventory.csv` and `MasterInventory.csv`
41//! files are. You can grab sample ones from the github repo.
42//!
43//! Optionally you can pass in custom filenames with flags after the `generate` command:
44//!
45//! ```terminal
46//! $ inventory_mgt generate -m masterinv.csv -s supplyinv.csv
47//! ```
48//!
49//! That's it! Enjoy!
50
51#[macro_use]
52extern crate structopt;
53
54#[macro_use]
55extern crate serde_derive;
56
57use csv;
58
59use structopt::StructOpt;
60use std::error::Error;
61use std::str;
62
63use indexmap::map::IndexMap;
64
65/**
66 * Command Line Parsing
67 */
68
69#[derive(StructOpt, Debug)]
70#[structopt(
71    name = "inventorymgt",
72    about = "Updates Master Inventory from Supply List CSV.",
73    long_about = "You can use this terminal program to populate a new csv, products from master inventory and quantitties from the newest supply inventory csv."
74)]
75/// InventoryConfig provides a structure for structopt to take in commands.
76pub enum InventoryConfig {
77    #[structopt(name = "generate")]
78    /// Generates a new csv file with updated quantities from the supply csv.
79    Generate {
80        /// Specifies the filename for master inventory csv and defaults to MasterInventory.csv.
81        #[structopt(short = "m", long = "master", default_value = "MasterInventory.csv")]
82        master_filename: String,
83
84        /// specifies the filename for supply inventory csv and defaults to SupplyInventory.csv
85        #[structopt(short = "s", long = "supply", default_value = "SupplyInventory.csv")]
86        supply_filename: String,
87    },
88}
89
90/**
91 * CSV Structs
92 */
93
94/// Provides the structures for a part from the Master csv list
95#[derive(Debug, PartialEq, Serialize, Deserialize)]
96#[serde(rename_all = "PascalCase")]
97pub struct MasterPart {
98    pub ven_code: String,
99    pub part_number: String,
100    #[serde(rename = "SKU")]
101    pub sku: String,
102    #[serde(deserialize_with = "csv::invalid_option")]
103    pub total_qty: Option<i32>,
104}
105
106impl MasterPart {
107
108    /// Updates total_qty with a supplied quantity, mutating the instance in place
109    pub fn update_qty(&mut self, qty: i32) {
110        self.total_qty = Some(qty);
111    }
112}
113
114#[derive(Debug, PartialEq, Deserialize)]
115#[serde(rename_all = "PascalCase")]
116struct SupplyPart<'a> {
117    ven_code: &'a str,
118    part_number: &'a [u8],
119    total_qty: i32,
120}
121
122/**
123 * MasterCache & Methods
124 */
125
126/// Holds the master inventory in a hashmap, where the key is the ven_code, and the value is a
127/// MasterPart struct.  This is for efficiency when searching through the master cache.
128pub struct MasterCache {
129    pub products: IndexMap<String, Vec<MasterPart>>
130}
131
132impl MasterCache {
133
134    /// Takes in a filename for a MasterInventory csv file, and on success, returns
135    /// a MasterCache struct instance.
136    pub fn from(filename: &str) -> Result<MasterCache, Box<dyn Error>> {
137        let mut rdr = csv::Reader::from_path(filename)?;
138        let mut products: IndexMap<String, Vec<MasterPart>> = IndexMap::new();
139
140        for result in rdr.deserialize() {
141            let product: MasterPart = result?;
142            let ven_code = product.ven_code.clone();
143
144            if let Some(v_code) = products.get_mut(&ven_code) {
145                v_code.push(product);
146            } else {
147                products.insert(ven_code, vec![product]);
148            }
149        }
150
151        Ok(MasterCache {
152            products,
153        })
154    }
155
156    /// Writes a new csv file from a MasterCache instance
157    pub fn write_csv(&self, filename: &'static str) -> Result<(), Box<dyn Error>> {
158        let mut wtr = csv::Writer::from_path(filename)?;
159
160        for products in self.products.values() {
161            for product in products {
162                wtr.serialize(product)?;
163            }
164        }
165
166        wtr.flush()?;
167        Ok(())
168    }
169}
170
171/// Creates a new master csv file called "newmaster.csv" with the updated quantities, pulled
172/// from the supply csv file
173pub fn update_master(master_filename: String, supply_filename: String) -> Result<(), Box<dyn Error>> {
174    let mut master_cache = MasterCache::from(&master_filename)?;
175
176    let mut rdr = csv::Reader::from_path(supply_filename)?;
177    let mut raw_record = csv::ByteRecord::new();
178    let headers = rdr.byte_headers()?.clone();
179
180    while rdr.read_byte_record(&mut raw_record)? {
181        let product: SupplyPart = raw_record.deserialize(Some(&headers))?;
182        let ven_code = product.ven_code;
183        let product_qty = product.total_qty;
184        
185        if let Some(v_code) = master_cache.products.get_mut(ven_code) {
186            
187            // this ven_code is in our master_cache so let's see
188            // if the product is there and update it's quantity
189            if let Some(master_product) = v_code.iter_mut().find(|p| p.part_number == str::from_utf8(product.part_number).unwrap()) {
190                master_product.update_qty(product_qty);
191            }
192        }
193    }
194    
195
196    // lastly let's write the updated master supply list
197    master_cache.write_csv("newmaster.csv")?;
198
199    Ok(())
200}
201
202/// `run` will take in an InventoryConfig enum config (parsed in `main`) and execute the appropriate
203/// program logic
204pub fn run(config: InventoryConfig) -> Result<(), Box<dyn Error>> {
205    match config {
206        InventoryConfig::Generate { master_filename, supply_filename } => update_master(master_filename, supply_filename)?,
207    }
208
209    Ok(())
210}