Initial commit
This commit is contained in:
115
Cargo.lock
generated
Normal file
115
Cargo.lock
generated
Normal file
@@ -0,0 +1,115 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
|
||||
dependencies = [
|
||||
"csv-core",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv-core"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"csv",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
8
Cargo.toml
Normal file
8
Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "inventory"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
csv = "1.4.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
162
README.md
Normal file
162
README.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# EEnventory - Electronics Component Inventory Helper
|
||||
|
||||
A command-line tool for managing your electronics component inventory for PCB projects. This tool helps you track parts, subtract BOMs when building PCBs, load Digikey orders, and identify what parts you need to order.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
I make a lot of PCBs and keep an inventory system for all of my components. It's tedious to keep a running count of all of my parts every time I receive a new Digikey order or build a PCB. Often times I just order the entire BOM for my PCB even though I know I'm ordering duplicates of some things, because I don't want to go individually count if I have enough.
|
||||
|
||||
This tool solves these problems by automating inventory management through CSV files.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. I keep a single Inventory.csv file that contains every part I would use in a PCB. Every part gets an internal part number for my reference. The CSV also lists the Digikey part number and quantity I have on hand. The key point is that I only have to count everything **once** at the beginning.
|
||||
|
||||
2. I keep a KiCad schematic symbol library updated with all inventoried parts, including the internal part number field, and the Digikey part number. Whatever CAD software you would use just needs to be able to include these extra fields in your schematic BOM when you export it.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Check What You Need to Order
|
||||
Before ordering parts, use the `check` command to verify if you have everything needed for a BOM. If parts are missing, the tool generates a `missing_parts.csv` file listing exactly what you need to order. **You can import this file directly into Digikey** to create a cart with your needed parts.
|
||||
|
||||
### 2. Load Digikey Orders
|
||||
When you receive a Digikey order, use the `add` command to bulk-load all parts into your inventory. When you download the CSV of your order from Digikey after it has been placed, **this tool can import it directly.** The tool matches parts by Digikey part number and automatically adds quantities to existing inventory entries.
|
||||
|
||||
### 3. Subtract BOMs When Building PCBs
|
||||
When you build a PCB, use the `deductbom` command to automatically subtract all parts from your inventory. When I export a BOM containing these field using Kicad, **this tool can import it directly.** That means if I build a PCB, I can deduct all of the parts I used with a single action based on the BOM. The tool will show you what's being deducted and ask for confirmation before making changes.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
This is a Rust project. To build and install:
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The binary will be in `target/release/inventory`. You can add it to your PATH or use it directly.
|
||||
|
||||
### Commands
|
||||
|
||||
#### `add` - Add Parts from Digikey Order
|
||||
Load parts from a Digikey order CSV file into your inventory.
|
||||
|
||||
```bash
|
||||
inventory add <inventory_path> <received_parts_path>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
inventory add inventory.csv ~/Downloads/DK_PRODUCTS_96012156.csv
|
||||
```
|
||||
|
||||
The tool will:
|
||||
- Match parts by Digikey part number
|
||||
- Add quantities to existing inventory entries
|
||||
- Show you what's changing before committing
|
||||
|
||||
#### `deduct` - Deduct a Single Part
|
||||
Remove a specific quantity of a single part from inventory.
|
||||
|
||||
```bash
|
||||
inventory deduct <inventory_path> <part_number> <quantity>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
inventory deduct inventory.csv RES-1000 5
|
||||
```
|
||||
|
||||
#### `deductbom` - Deduct a BOM
|
||||
Subtract all parts from a Bill of Materials (BOM) CSV file from your inventory.
|
||||
|
||||
```bash
|
||||
inventory deductbom <inventory_path> <bom_path>
|
||||
```
|
||||
|
||||
The tool will:
|
||||
- Check that all parts are available in sufficient quantities
|
||||
- Show you what's being deducted
|
||||
- Ask for confirmation before updating inventory
|
||||
|
||||
#### `check` - Check BOM Availability
|
||||
Verify if you have all parts needed for a BOM. If parts are missing, generates a `missing_parts.csv` file. **You can import this csv directly into Digikey to order a cart with these parts.**
|
||||
|
||||
```bash
|
||||
inventory check <inventory_path> <bom_path>
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
inventory check inventory.csv my_pcb_bom.csv
|
||||
```
|
||||
|
||||
If parts are missing, the tool will:
|
||||
- Print "You are missing some parts."
|
||||
- Generate `missing_parts.csv` with the exact quantities you need to order
|
||||
|
||||
If all parts are available, it prints "BOM is available."
|
||||
|
||||
|
||||
### Inventory File Format
|
||||
|
||||
Your inventory CSV file should have the following columns (flexible naming is supported):
|
||||
|
||||
- **Internal P/N** (or `Part Number`, `PN`, `internal_pn`) - Your internal part number (required)
|
||||
- **Manufacturer P/N** (or `Manufacturer Part Number`, `manufacturer_pn`) - Optional
|
||||
- **Digikey P/N** (or `Digikey PN`, `DigiKey Part #`, `digikey_pn`) - Optional, but required for `add` command
|
||||
- **Value** (or `value`) - Component value (e.g., "10k", "100uF") - Optional
|
||||
- **Description** (or `description`) - Optional
|
||||
- **Quantity** (or `Qty`, `quantity`) - Current quantity on hand
|
||||
- **Notes** (or `notes`) - Optional
|
||||
|
||||
### BOM File Format
|
||||
|
||||
The rules for reading a BOM follow the same rules as a regular inventory: It's a CSV with the same header requirements.
|
||||
|
||||
### Digikey Order CSV Format
|
||||
|
||||
For the `add` command, the CSV should have:
|
||||
- **DigiKey Part #** (or `Digikey P/N`, `Digikey PN`, `digikey_pn`) - Required
|
||||
- **Quantity** (or `Qty`, `quantity`) - Required
|
||||
|
||||
The tool matches these parts to your inventory by Digikey part number.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Part Matching**:
|
||||
- For `add`: Parts are matched by Digikey part number
|
||||
- For `deductbom` and `check`: Parts are matched by Internal P/N
|
||||
|
||||
2. **Safety Features**:
|
||||
- All changes are shown before committing
|
||||
- You must confirm changes with 'y' before inventory is updated
|
||||
- Errors prevent partial updates (all-or-nothing for batch operations)
|
||||
|
||||
3. **Error Handling**:
|
||||
- If a part isn't found, the operation fails
|
||||
- If quantities are insufficient, the operation fails with clear error messages
|
||||
- Multiple errors are collected and reported together
|
||||
|
||||
## Example Workflow
|
||||
|
||||
1. **Initial Setup**: Create your inventory CSV file with all your parts and their current quantities.
|
||||
|
||||
2. **Receiving a Digikey Order**:
|
||||
```bash
|
||||
inventory add inventory.csv ~/Downloads/DK_PRODUCTS_96012156.csv
|
||||
```
|
||||
Review the changes and confirm with 'y'.
|
||||
|
||||
3. **Planning a Build**:
|
||||
```bash
|
||||
inventory check inventory.csv my_pcb_bom.csv
|
||||
```
|
||||
If parts are missing, order them using `missing_parts.csv`.
|
||||
|
||||
4. **After Building a PCB**:
|
||||
```bash
|
||||
inventory deductbom inventory.csv my_pcb_bom.csv
|
||||
```
|
||||
Review what was deducted and confirm.
|
||||
296
src/inventory.rs
Normal file
296
src/inventory.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
|
||||
pub(crate) type Inventory = HashMap<String, InventoriedPart>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InventoryError {
|
||||
NotEnoughQuantity {
|
||||
part_number: String,
|
||||
onhand_quantity: u64,
|
||||
requested_quantity: u64,
|
||||
},
|
||||
QuantityNotFound {
|
||||
part_number: String,
|
||||
},
|
||||
PartNotFound {
|
||||
part_number: String,
|
||||
},
|
||||
MultipleErrors {
|
||||
errors: Vec<InventoryError>,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct InventoriedPart {
|
||||
// KiCad exports as "PN"
|
||||
#[serde(rename = "Internal P/N", alias = "Part Number", alias = "PN", alias = "internal_pn")]
|
||||
pub internal_pn: String,
|
||||
|
||||
// Digikey exports as "Manufacturer Part Number"
|
||||
#[serde(rename = "Manufacturer P/N", alias = "Manufacturer Part Number", alias = "manufacturer_pn")]
|
||||
pub manufacturer_pn: Option<String>,
|
||||
|
||||
// "Kicad exports as "Digikey PN"
|
||||
// Digikey exports as "DigiKey Part #"
|
||||
#[serde(rename = "Digikey P/N", alias = "Digikey PN", alias = "DigiKey Part #", alias = "digikey_pn")]
|
||||
pub digikey_pn: Option<String>,
|
||||
|
||||
#[serde(rename = "Value", alias = "value")]
|
||||
pub value: Option<String>,
|
||||
|
||||
#[serde(rename = "Description", alias = "description")]
|
||||
pub description: Option<String>,
|
||||
|
||||
// KiCad exports as "Qty"
|
||||
// Digikey exports as "Quantity"
|
||||
#[serde(rename = "Quantity", alias = "Qty", alias = "quantity")]
|
||||
pub quantity: Option<u64>,
|
||||
|
||||
#[serde(rename = "Notes", alias = "notes")]
|
||||
pub notes: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for InventoriedPart {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let manufacturer_pn = self.manufacturer_pn.clone().unwrap_or("".to_string());
|
||||
let digikey_pn = self.digikey_pn.clone().unwrap_or("".to_string());
|
||||
let quantity = self.quantity.unwrap_or(0);
|
||||
write!(f, "{} | {} | {} ({}x)", self.internal_pn, manufacturer_pn, digikey_pn, quantity)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ReceivedPart {
|
||||
|
||||
// Digikey exports as "DigiKey Part #"
|
||||
#[serde(rename = "DigiKey Part #", alias = "Digikey P/N", alias = "Digikey PN", alias = "digikey_pn")]
|
||||
pub digikey_pn: String,
|
||||
|
||||
// Digikey exports as "Quantity"
|
||||
#[serde(rename = "Quantity", alias = "Qty", alias = "quantity")]
|
||||
pub quantity: u64,
|
||||
|
||||
}
|
||||
|
||||
pub fn print_inventory(inventory: &Inventory) {
|
||||
for (_, part) in inventory.iter() {
|
||||
println!("{}", part);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_inventory_file(path: &str) -> Result<Inventory, Box<dyn std::error::Error>> {
|
||||
let file = File::open(path)?;
|
||||
let mut reader = csv::Reader::from_reader(file);
|
||||
let mut parts: Inventory = HashMap::new();
|
||||
for result in reader.deserialize() {
|
||||
let part: InventoriedPart = result?;
|
||||
parts.insert(part.internal_pn.clone(), part);
|
||||
}
|
||||
|
||||
Ok(parts)
|
||||
}
|
||||
|
||||
pub fn write_inventory_file_from_vector(parts: &Vec<InventoriedPart>, path: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file = File::create(path)?;
|
||||
let mut writer = csv::Writer::from_writer(file);
|
||||
for part in parts {
|
||||
writer.serialize(part)?;
|
||||
}
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_inventory_file(inventory: &Inventory, path: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let file = File::create(path)?;
|
||||
let mut writer = csv::Writer::from_writer(file);
|
||||
for (_, part) in inventory.iter() {
|
||||
writer.serialize(part)?;
|
||||
}
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read a Digikey Order CSV file and return a vector of ReceivedParts.
|
||||
pub fn read_received_parts_file(path: &str) -> Result<Vec<ReceivedPart>, Box<dyn std::error::Error>> {
|
||||
let file = File::open(path)?;
|
||||
let mut reader = csv::Reader::from_reader(file);
|
||||
let mut parts: Vec<ReceivedPart> = Vec::new();
|
||||
for result in reader.deserialize() {
|
||||
if let Ok(part) = result {
|
||||
parts.push(part);
|
||||
}
|
||||
}
|
||||
Ok(parts)
|
||||
}
|
||||
|
||||
pub fn print_staged_changes(old_inventory: &Inventory, new_inventory: &Inventory) {
|
||||
println!("Staged changes:");
|
||||
for (_, part) in new_inventory.iter() {
|
||||
if let Some(old_part) = old_inventory.get(&part.internal_pn) {
|
||||
if old_part.quantity != part.quantity {
|
||||
println!(
|
||||
"{: <20} | {: >10} -> {: <10}",
|
||||
part.internal_pn,
|
||||
old_part.quantity.unwrap_or(0),
|
||||
part.quantity.unwrap_or(0)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Add a received part to the inventory.
|
||||
/// Returns an error if the part is not found in the inventory.
|
||||
pub fn add_received_part(inventory: &mut Inventory, received_part: ReceivedPart) -> Result<(), InventoryError> {
|
||||
for inventoried_part in inventory.values_mut() {
|
||||
let Some(inventoried_digikey_pn) = inventoried_part.digikey_pn.clone() else { continue };
|
||||
|
||||
if inventoried_digikey_pn != received_part.digikey_pn { continue };
|
||||
|
||||
let quantity = match inventoried_part.quantity {
|
||||
Some(q) => q,
|
||||
None => {
|
||||
println!("Inventoried part {} has no quantity", inventoried_part.internal_pn);
|
||||
return Err(InventoryError::QuantityNotFound { part_number: inventoried_part.internal_pn.clone() });
|
||||
}
|
||||
};
|
||||
let new_quantity = quantity + received_part.quantity;
|
||||
inventoried_part.quantity = Some(new_quantity);
|
||||
return Ok(());
|
||||
}
|
||||
Err(InventoryError::PartNotFound { part_number: received_part.digikey_pn.clone() })
|
||||
}
|
||||
|
||||
/// Add a vector of received parts to the inventory.
|
||||
/// Returns an error if any part is not found in the inventory.
|
||||
/// Changes are only committed to the inventory if all parts are added successfully.
|
||||
pub fn add_received_parts(inventory: &mut Inventory, new_parts: Vec<ReceivedPart>) -> Result<(), InventoryError> {
|
||||
|
||||
let mut staged_inventory = inventory.clone();
|
||||
for part in new_parts {
|
||||
add_received_part(&mut staged_inventory, part)?;
|
||||
}
|
||||
*inventory = staged_inventory;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the given quantity is available in the inventory.
|
||||
/// Returns the remaining quantity if the check is successful.
|
||||
pub fn check_part(inventory: &Inventory, part_number: &str, quantity: u64) -> Result<u64, InventoryError> {
|
||||
let part = inventory.get(part_number).ok_or(InventoryError::PartNotFound { part_number: part_number.to_string() })?;
|
||||
let onhand_quantity = part.quantity.ok_or(InventoryError::QuantityNotFound { part_number: part_number.to_string() })?;
|
||||
if onhand_quantity < quantity {
|
||||
return Err(InventoryError::NotEnoughQuantity { part_number: part_number.to_string(), onhand_quantity: onhand_quantity, requested_quantity: quantity });
|
||||
}
|
||||
Ok(onhand_quantity)
|
||||
|
||||
}
|
||||
|
||||
pub fn check_parts(inventory: &Inventory, parts: &Vec<InventoriedPart>) -> Result<(), InventoryError> {
|
||||
let mut errors = Vec::new();
|
||||
for part in parts {
|
||||
let requested_quantity = match part.quantity {
|
||||
Some(q) => q,
|
||||
None => {
|
||||
println!("Checked part {} has no quantity", part.internal_pn);
|
||||
return Err(InventoryError::QuantityNotFound { part_number: part.internal_pn.to_string() });
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(error) = check_part(inventory, &part.internal_pn, requested_quantity) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
if errors.len() == 1 {
|
||||
return Err(errors[0].clone());
|
||||
}
|
||||
else {
|
||||
return Err(InventoryError::MultipleErrors { errors: errors });
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deduct the given quantity from the part.
|
||||
/// Returns the remaining quantity if the deduction is successful.
|
||||
pub fn deduct_part(inventory: &mut Inventory, part_number: &str, quantity: u64) -> Result<u64, InventoryError> {
|
||||
if let Some(part) = inventory.get_mut(part_number) {
|
||||
|
||||
let onhand_quantity = part.quantity.ok_or(InventoryError::QuantityNotFound { part_number: part_number.to_string() })?;
|
||||
if onhand_quantity < quantity {
|
||||
return Err(InventoryError::NotEnoughQuantity { part_number: part_number.to_string(), onhand_quantity: onhand_quantity, requested_quantity: quantity });
|
||||
}
|
||||
part.quantity = Some(onhand_quantity - quantity);
|
||||
return Ok(onhand_quantity - quantity);
|
||||
}
|
||||
Err(InventoryError::PartNotFound { part_number: part_number.to_string() })
|
||||
}
|
||||
|
||||
/// Deduct the given parts from the inventory.
|
||||
/// Returns an error if any part is not found or if the quantity is not enough.
|
||||
/// If all parts are deducted successfully, the inventory is committed.
|
||||
pub fn deduct_parts(inventory: &mut Inventory, parts: &Vec<InventoriedPart>) -> Result<(), InventoryError> {
|
||||
|
||||
check_parts(inventory, &parts)?;
|
||||
for part in parts {
|
||||
deduct_part(inventory, &part.internal_pn, part.quantity.unwrap_or(0))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_missing_bom(inventory: &Inventory, bom: &Vec<InventoriedPart>) -> Result<Vec<InventoriedPart>, InventoryError> {
|
||||
let mut missing_parts = Vec::new();
|
||||
|
||||
for part in bom {
|
||||
let requested_quantity = match part.quantity {
|
||||
Some(q) => q,
|
||||
None => {
|
||||
println!("BOM part {} has no quantity", part.internal_pn);
|
||||
return Err(InventoryError::QuantityNotFound { part_number: part.internal_pn.to_string() });
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(error) = check_part(inventory, &part.internal_pn, requested_quantity) {
|
||||
match error {
|
||||
InventoryError::NotEnoughQuantity { part_number, onhand_quantity, requested_quantity } => {
|
||||
|
||||
let missing_quantity: u64 = requested_quantity - onhand_quantity;
|
||||
let mut part: InventoriedPart = match inventory.get(&part_number) {
|
||||
Some(part) => part.clone(),
|
||||
None => InventoriedPart {
|
||||
internal_pn: part_number.to_string(),
|
||||
manufacturer_pn: None,
|
||||
digikey_pn: None,
|
||||
value: None,
|
||||
description: None,
|
||||
notes: None,
|
||||
quantity: Some(missing_quantity),
|
||||
},
|
||||
};
|
||||
part.quantity = Some(missing_quantity);
|
||||
part.notes = None;
|
||||
missing_parts.push(part);
|
||||
},
|
||||
InventoryError::PartNotFound { part_number } => {
|
||||
let mut part: InventoriedPart = part.clone();
|
||||
part.quantity = Some(requested_quantity);
|
||||
part.notes = Some(format!("Part number not found in inventory!"));
|
||||
missing_parts.push(part);
|
||||
},
|
||||
InventoryError::QuantityNotFound { part_number } => {
|
||||
let mut part: InventoriedPart = part.clone();
|
||||
part.quantity = Some(requested_quantity);
|
||||
part.notes = Some(format!("Quantity not found in inventory!"));
|
||||
missing_parts.push(part);
|
||||
},
|
||||
_ => return Err(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(missing_parts)
|
||||
}
|
||||
144
src/main.rs
Normal file
144
src/main.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
mod inventory;
|
||||
|
||||
pub fn confirm_changes(inventory_path: &str, original_inventory: &inventory::Inventory, staged_inventory: &inventory::Inventory) {
|
||||
inventory::print_staged_changes(original_inventory, staged_inventory);
|
||||
println!("Commit changes? (y/n)");
|
||||
let mut commit = String::new();
|
||||
std::io::stdin().read_line(&mut commit).expect("Error reading input");
|
||||
if commit.trim() == "y" {
|
||||
inventory::write_inventory_file(staged_inventory, &inventory_path).expect("Error writing inventory file");
|
||||
println!("Inventory updated.");
|
||||
} else {
|
||||
println!("Inventory not updated.");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_command( inventory_path: &str, received_parts_path: &str) {
|
||||
let original_inventory = inventory::read_inventory_file(&inventory_path).expect("Error reading inventory file");
|
||||
let mut staged_inventory = original_inventory.clone();
|
||||
let received_parts = inventory::read_received_parts_file(&received_parts_path).expect("Error reading received parts file");
|
||||
let result = inventory::add_received_parts(&mut staged_inventory, received_parts);
|
||||
if let Err(error) = result {
|
||||
println!("Error adding parts: {:?}", error);
|
||||
println!("Inventory not updated.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
confirm_changes(inventory_path, &original_inventory, &staged_inventory);
|
||||
}
|
||||
|
||||
pub fn deduct_command( inventory_path: &str, part: &str, quantity: u64) {
|
||||
let original_inventory = inventory::read_inventory_file(&inventory_path).expect("Error reading inventory file");
|
||||
let mut staged_inventory = original_inventory.clone();
|
||||
|
||||
println!("Deducting {} {} from inventory...", part, quantity);
|
||||
|
||||
match inventory::deduct_part(&mut staged_inventory, &part, quantity) {
|
||||
Ok(remaining_quantity) => {
|
||||
println!("Remaining quantity: {}", remaining_quantity);
|
||||
}
|
||||
Err(error) => {
|
||||
println!("Error deducting part: {:?}", error);
|
||||
println!("Inventory not updated.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(error) = inventory::write_inventory_file(&staged_inventory, &inventory_path) {
|
||||
println!("Error writing inventory file: {:?}", error);
|
||||
println!("Inventory not updated.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Confirm changes
|
||||
confirm_changes(inventory_path, &original_inventory, &staged_inventory);
|
||||
}
|
||||
|
||||
pub fn deductbom_command( inventory_path: &str, bom_path: &str) {
|
||||
let original_inventory = inventory::read_inventory_file(&inventory_path).expect("Error reading inventory file");
|
||||
let mut staged_inventory = original_inventory.clone();
|
||||
let bom = inventory::read_inventory_file(&bom_path).expect("Error reading bom file");
|
||||
let result = inventory::deduct_parts(&mut staged_inventory, &bom.values().cloned().collect());
|
||||
if let Err(error) = result {
|
||||
println!("Error deducting BOM: {:?}", error);
|
||||
println!("Inventory not updated.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Confirm changes
|
||||
confirm_changes(inventory_path, &original_inventory, &staged_inventory);
|
||||
}
|
||||
|
||||
pub fn checkbom_command( inventory_path: &str, bom_path: &str) {
|
||||
let inventory = inventory::read_inventory_file(&inventory_path).expect("Error reading inventory file");
|
||||
let bom = inventory::read_inventory_file(&bom_path).expect("Error reading bom file");
|
||||
|
||||
if let Ok(_) = inventory::check_parts(&inventory, &bom.values().cloned().collect()) {
|
||||
println!("BOM is available.");
|
||||
} else {
|
||||
println!("You are missing some parts.");
|
||||
let missing_parts = inventory::generate_missing_bom(&inventory, &bom.values().cloned().collect()).expect("Error generating missing BOM");
|
||||
inventory::write_inventory_file_from_vector(&missing_parts, "missing_parts.csv").expect("Error writing inventory file");
|
||||
println!("Missing parts written to missing_parts.csv");
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
// let args: Vec<String> = vec!["inventory".to_string(), "add".to_string(), "/home/naly/Documents/KiCad/inventory.csv".to_string(), "/home/naly/Downloads/DK_PRODUCTS_96012156.csv".to_string()];
|
||||
|
||||
if args.len() < 3 {
|
||||
println!("Usage: inventory <command>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let command = args[1].clone();
|
||||
match command.as_str() {
|
||||
"add" => {
|
||||
if args.len() != 4 {
|
||||
println!("Usage: inventory add <inventory_path> <received_parts_path>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let inventory_path = args[2].clone();
|
||||
let received_parts_path = args[3].clone();
|
||||
add_command(&inventory_path, &received_parts_path);
|
||||
},
|
||||
"deduct" => {
|
||||
if args.len() != 5 {
|
||||
println!("Usage: inventory deduct <inventory_path> <part> <quantity>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let inventory_path = args[2].clone();
|
||||
let part = args[3].clone();
|
||||
let quantity = args[3].parse::<u64>().expect("Quantity must be a number");
|
||||
deduct_command(&inventory_path, &part, quantity);
|
||||
}
|
||||
"deductbom" => {
|
||||
if args.len() != 4 {
|
||||
println!("Usage: inventory deductbom <inventory_path> <bom_path>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let inventory_path = args[2].clone();
|
||||
let bom_path = args[3].clone();
|
||||
deductbom_command(&inventory_path, &bom_path);
|
||||
},
|
||||
"check" => {
|
||||
if args.len() != 4 {
|
||||
println!("Usage: inventory check <inventory_path> <bom_path>");
|
||||
std::process::exit(1);
|
||||
}
|
||||
let inventory_path = args[2].clone();
|
||||
let bom_path = args[3].clone();
|
||||
checkbom_command(&inventory_path, &bom_path);
|
||||
},
|
||||
_ => {
|
||||
println!("Usage: inventory <command>");
|
||||
println!("Commands: add, deduct, deductbom, check");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user