diff --git a/src/app/adjustment.rs b/src/app/adjustment.rs index 591010c..e9afd08 100644 --- a/src/app/adjustment.rs +++ b/src/app/adjustment.rs @@ -11,11 +11,12 @@ use tracing::info; use chrono::FixedOffset; use crate::db::adjustment::{add_adjustment, get_item_adjustment_valuation_weighted_mean, DbAdjustmentWithValuation}; use crate::db::adjustment::adjustment_reason::DbAdjustmentReason; -use crate::db::inventory_item::inventory_item_get_by_id_with_unit; +use crate::db::inventory_item::{does_inventory_item_allow_fractional_units, inventory_item_get_by_id_with_unit}; use crate::error::AppError; use crate::session::SessionUser; use crate::util::currency; -use crate::util::currency::int_cents_to_dollars_string; +use crate::util::currency::{dollars_string_to_int_cents, int_cents_to_dollars_string}; +use crate::util::formatting::format_either; #[derive(Template)] @@ -32,64 +33,161 @@ pub async fn get_adjustments_for_item(State(db): State, let timezone = user.get_timezone()?; + let allow_fractional = + does_inventory_item_allow_fractional_units(&db, id).await?; + let adjustments: Vec = get_item_adjustment_valuation_weighted_mean(&db, id).await?; let adjustments: Vec = adjustments.into_iter() - .map(|x| AdjustmentDisplayItem::from((x, timezone))) + .map(|x| AdjustmentDisplayItem::from_db_item(x, timezone, allow_fractional)) .collect(); Ok(AdjustmentTableTemplate { item_id: id, adjustments }.into_response()) } #[derive(Template)] -#[template(path = "adjustments-add-form.html")] -pub struct AdjustmentAddFormTemplate { +#[template(path = "adjustment-sale-form.html")] +pub struct AdjustmentSaleFormTemplate { pub item_id: i64, + pub amount_error: &'static str, } #[derive(Deserialize, Debug)] -pub struct AddAdjustmentFormData { +pub struct AdjustmentSaleFormData { pub amount: f64, - pub reason: Option, } #[debug_handler] -pub async fn adjustment_add_form_post(State(db): State, - Path(id): Path, - user: SessionUser, - form_data: Form +pub async fn adjustment_sale_form_post(State(db): State, + Path(id): Path, + user: SessionUser, + form_data: Form ) -> Result { + let adjustment_amount = if form_data.amount > 0.0 { + -1.0 * form_data.amount + } else { + form_data.amount + }; + + let fractional_units_allowed = + does_inventory_item_allow_fractional_units(&db, id).await?; + + if adjustment_amount == 0.0 { + return Ok(AdjustmentSaleFormTemplate { + item_id: id, + amount_error: "Please input a non-zero amount" + }.into_response()) + } else if !(fractional_units_allowed || adjustment_amount.fract() == 0.0) { + return Ok(AdjustmentSaleFormTemplate { + item_id: id, + amount_error: "Please input a whole number" + }.into_response()) + } + let trigger_events = HxResponseTrigger::normal( std::iter::once(HxEvent::from("new-adjustment")) ); let timestamp = chrono::Utc::now(); - let adjustment_amount = if form_data.amount > 0.0 { - -1.0 * form_data.amount + info!("Add adjustment form: {:?} for user {}", form_data, user.name); + + let _new_id = add_adjustment(&db, id, user.id, + timestamp, + timestamp, + adjustment_amount, + None, + DbAdjustmentReason::Sale).await?; + + + Ok((trigger_events, AdjustmentSaleFormTemplate { item_id: id, amount_error: "" }.into_response()).into_response()) +} + +pub async fn adjustment_sale_form_get( + Path(id): Path +) -> Result { + Ok(AdjustmentSaleFormTemplate { item_id: id, amount_error: "" }.into_response()) +} + +#[derive(Template)] +#[template(path = "adjustment-new-stock-form.html")] +pub struct AdjustmentNewStockFormTemplate { + pub item_id: i64, + pub amount_error: &'static str, + pub price_error: &'static str, +} + +#[derive(Deserialize, Debug)] +pub struct AdjustmentNewStockFormData { + pub amount: f64, + pub price: String, +} + +#[debug_handler] +pub async fn adjustment_new_stock_form_post(State(db): State, + Path(id): Path, + user: SessionUser, + form_data: Form +) -> Result { + + let fractional_units_allowed = + does_inventory_item_allow_fractional_units(&db, id).await?; + + let amount_error = if form_data.amount == 0.0 { + "Please input a non-zero amount" + } else if !(fractional_units_allowed || form_data.amount.fract() == 0.0) { + "Please input a whole number" } else { - form_data.amount + "" }; + let price = dollars_string_to_int_cents(&form_data.price); + let price_error = if price.is_err() { + "Please input a dollar amount" + } else { + "" + }; + + if !(amount_error.is_empty() && price_error.is_empty()) { + return Ok(AdjustmentNewStockFormTemplate { + item_id: id, + amount_error, + price_error, + }.into_response()) + } + + let price = price?; + let unit_price = (price as f64 / form_data.amount) as i64; + + let trigger_events = HxResponseTrigger::normal( + std::iter::once(HxEvent::from("new-adjustment")) + ); + + let timestamp = chrono::Utc::now(); + + let adjustment_amount = form_data.amount; + info!("Add adjustment form: {:?} for user {}", form_data, user.name); let _new_id = add_adjustment(&db, id, user.id, timestamp, timestamp, adjustment_amount, - None, - DbAdjustmentReason::Sale).await?; + Some(unit_price), + DbAdjustmentReason::NewStock).await?; - Ok((trigger_events, AdjustmentAddFormTemplate { item_id: id }.into_response()).into_response()) + Ok((trigger_events, AdjustmentNewStockFormTemplate { item_id: id, + amount_error: "", price_error: "" }.into_response()).into_response()) } -pub async fn adjustment_add_form_get( +pub async fn adjustment_new_stock_form_get( Path(id): Path ) -> Result { - Ok(AdjustmentAddFormTemplate { item_id: id }.into_response()) + Ok(AdjustmentNewStockFormTemplate { item_id: id, + amount_error: "", price_error: "" }.into_response()) } @@ -103,17 +201,18 @@ pub struct AdjustmentDisplayItem { pub reason: String, } -impl From<(DbAdjustmentWithValuation, FixedOffset)> for AdjustmentDisplayItem { - fn from(item: (DbAdjustmentWithValuation, FixedOffset)) -> Self { - let db_entry = item.0; - let timezone = item.1; + +impl AdjustmentDisplayItem { + pub fn from_db_item(db_entry: DbAdjustmentWithValuation, timezone: FixedOffset + , allow_fractional: bool) -> Self { + let precision : usize = if allow_fractional { 2 } else { 0 }; let date = db_entry.adjustment.target_date.with_timezone(&timezone) .format("%Y-%m-%d %l:%M:%S %p").to_string(); - let amount = db_entry.adjustment.amount.to_string(); + let amount = format!("{:.*}", precision, db_entry.adjustment.amount); let unit_price = db_entry.adjustment.unit_price.unwrap_or_else(|| - (db_entry.value as f64 / db_entry.tally) as i64); + (db_entry.value as f64 / db_entry.tally) as i64); let unit_value = currency::int_cents_to_dollars_string(unit_price); - let tally = db_entry.tally.to_string(); + let tally = format!("{:.*}", precision, db_entry.tally); let tally_value = currency::int_cents_to_dollars_string(db_entry.value); let reason = db_entry.adjustment.reason.into(); @@ -125,5 +224,6 @@ impl From<(DbAdjustmentWithValuation, FixedOffset)> for AdjustmentDisplayItem { tally_value, reason, } + } -} \ No newline at end of file +} diff --git a/src/app/item.rs b/src/app/item.rs index 3e27edb..f5bdcfe 100644 --- a/src/app/item.rs +++ b/src/app/item.rs @@ -9,6 +9,7 @@ use crate::db::inventory_item::{inventory_item_get_by_id_with_unit, DbInventoryI use crate::error::AppError; use crate::session::SessionUser; use crate::util::currency::int_cents_to_dollars_string; +use crate::util::formatting::format_either; #[derive(Template)] #[template(path = "item.html")] @@ -29,8 +30,8 @@ struct ItemDisplayItem { impl From for ItemDisplayItem { fn from(item: DbInventoryItemWithCount) -> Self { - let amount = item.amount.unwrap_or_default().to_string(); - let value = int_cents_to_dollars_string(0); + let precision = if item.allow_fractional_units { 2 } else { 0 }; + let amount = format!("{:.*}", precision, item.amount.unwrap_or_default()); Self { name: item.name, reorder_point: item.reorder_point, diff --git a/src/app/mod.rs b/src/app/mod.rs index 9fa42bd..78881d5 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -30,8 +30,10 @@ pub fn routes() -> Router { .route("/item/:item_id/", get(item::item)) .route("/item/:item_id/count", get(item::item_count)) .route("/item/:item_id/adjustments", get(adjustment::get_adjustments_for_item)) - .route("/item/:item_id/adjustment/new", post(adjustment::adjustment_add_form_post)) - .route("/item/:item_id/adjustment/new", get(adjustment::adjustment_add_form_get)) + .route("/item/:item_id/adjustment/sale", post(adjustment::adjustment_sale_form_post)) + .route("/item/:item_id/adjustment/sale", get(adjustment::adjustment_sale_form_get)) + .route("/item/:item_id/adjustment/new-stock", post(adjustment::adjustment_new_stock_form_post)) + .route("/item/:item_id/adjustment/new-stock", get(adjustment::adjustment_new_stock_form_get)) .route("/upload", get(upload::index::upload_index_handler)) .route("/upload/catalog", post(upload::catalog::catalog_import)) .route("/overview", get(overview::overview_handler)) diff --git a/src/db/inventory_item.rs b/src/db/inventory_item.rs index 9579d98..4c5a081 100644 --- a/src/db/inventory_item.rs +++ b/src/db/inventory_item.rs @@ -134,3 +134,19 @@ pub async fn inventory_item_get_by_id_with_unit(db: &SqlitePool, id: i64) -> Res .fetch_one(db).await .map_err(From::from) } + +pub async fn does_inventory_item_allow_fractional_units(db: &SqlitePool, id: i64) -> Result { + let res = sqlx::query!( + r#" + SELECT + allow_fractional_units + FROM + InventoryItem + WHERE InventoryItem.id = ? + "#, + id + ) + .fetch_one(db).await?; + + Ok(res.allow_fractional_units) +} diff --git a/src/util/currency.rs b/src/util/currency.rs index 54f51bd..80b0669 100644 --- a/src/util/currency.rs +++ b/src/util/currency.rs @@ -1,6 +1,20 @@ +use anyhow::{anyhow, Result}; + pub fn int_cents_to_dollars_string(i: i64) -> String { let whole = i / 100; let cents = i % 100; format!("${}.{}", whole, cents) -} \ No newline at end of file +} + +pub fn dollars_string_to_int_cents(s: &str) -> Result { + let mut parts = s.split('.'); + let format_err = || anyhow!("Bad format"); + let whole = parts.next().ok_or_else(format_err)?; + let cents = parts.next().ok_or_else(format_err)?; + + let whole = whole.parse::()?; + let cents = cents.parse::()?; + + Ok(whole * 100 + cents) +} diff --git a/src/util/formatting.rs b/src/util/formatting.rs new file mode 100644 index 0000000..08ddc42 --- /dev/null +++ b/src/util/formatting.rs @@ -0,0 +1,11 @@ + +/// Call format! macro with two different format strings based on a boolean value +macro_rules! format_either { + ($switch:expr, $true_fmt:literal, $false_fmt:literal, $($args:expr),*) => { + match $switch { + true => format!($true_fmt, $($args),*), + false => format!($false_fmt, $($args),*), + } + }; +} +pub(crate) use format_either; diff --git a/src/util/mod.rs b/src/util/mod.rs index 53e2faf..59965db 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,3 +1,4 @@ pub mod time; pub mod currency; -pub mod parsing; \ No newline at end of file +pub mod parsing; +pub mod formatting; \ No newline at end of file diff --git a/templates/adjustment-new-stock-form.html b/templates/adjustment-new-stock-form.html new file mode 100644 index 0000000..4510840 --- /dev/null +++ b/templates/adjustment-new-stock-form.html @@ -0,0 +1,28 @@ + +
+
+ + + {% if !amount_error.is_empty() -%} + {{ amount_error }} + {% endif -%} +
+
+ + + {% if !price_error.is_empty() -%} + {{ price_error }} + {% endif -%} +
+ +
diff --git a/templates/adjustment-sale-form.html b/templates/adjustment-sale-form.html new file mode 100644 index 0000000..804a9a1 --- /dev/null +++ b/templates/adjustment-sale-form.html @@ -0,0 +1,16 @@ + +
+
+ + + {% if !amount_error.is_empty() -%} + {{ amount_error }} + {% endif -%} +
+ +
diff --git a/templates/adjustments-add-form.html b/templates/adjustments-add-form.html deleted file mode 100644 index 1b81e18..0000000 --- a/templates/adjustments-add-form.html +++ /dev/null @@ -1,12 +0,0 @@ - -
-
- - -
-
- - -
- -
diff --git a/templates/item.html b/templates/item.html index 970ed94..f6b46f2 100644 --- a/templates/item.html +++ b/templates/item.html @@ -11,7 +11,10 @@
-
+
+
+ +
{% endblock %} \ No newline at end of file