Positive and negative adjustments

demo-mode
Wes Holland 12 months ago
parent 08194c5775
commit b55d83d769

@ -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<SqlitePool>,
let timezone = user.get_timezone()?;
let allow_fractional =
does_inventory_item_allow_fractional_units(&db, id).await?;
let adjustments: Vec<DbAdjustmentWithValuation> = get_item_adjustment_valuation_weighted_mean(&db, id).await?;
let adjustments: Vec<AdjustmentDisplayItem> = 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<String>,
}
#[debug_handler]
pub async fn adjustment_add_form_post(State(db): State<SqlitePool>,
pub async fn adjustment_sale_form_post(State(db): State<SqlitePool>,
Path(id): Path<i64>,
user: SessionUser,
form_data: Form<AddAdjustmentFormData>
form_data: Form<AdjustmentSaleFormData>
) -> Result<Response, AppError> {
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<i64>
) -> Result<Response, AppError> {
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<SqlitePool>,
Path(id): Path<i64>,
user: SessionUser,
form_data: Form<AdjustmentNewStockFormData>
) -> Result<Response, AppError> {
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<i64>
) -> Result<Response, AppError> {
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);
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,
}
}
}

@ -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<DbInventoryItemWithCount> 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,

@ -30,8 +30,10 @@ pub fn routes() -> Router<AppState> {
.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))

@ -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<bool> {
let res = sqlx::query!(
r#"
SELECT
allow_fractional_units
FROM
InventoryItem
WHERE InventoryItem.id = ?
"#,
id
)
.fetch_one(db).await?;
Ok(res.allow_fractional_units)
}

@ -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)
}
pub fn dollars_string_to_int_cents(s: &str) -> Result<i64> {
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::<i64>()?;
let cents = cents.parse::<i64>()?;
Ok(whole * 100 + cents)
}

@ -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;

@ -1,3 +1,4 @@
pub mod time;
pub mod currency;
pub mod parsing;
pub mod formatting;

@ -0,0 +1,28 @@
<form hx-post="/item/{{item_id}}/adjustment/new-stock" hx-target="this" hx-swap="outerHTML" >
<div>
<label for="amount">Amount</label>
<input id="amount" name="amount" type="number" required
{% if !amount_error.is_empty() -%}
aria-invalid="true"
aria-describedby="invalid-amount"
{% endif -%}
/>
{% if !amount_error.is_empty() -%}
<small id="invalid-amount">{{ amount_error }}</small>
{% endif -%}
</div>
<div>
<label for="price">Price</label>
<input id="price" name="price" required
{% if !price_error.is_empty() -%}
aria-invalid="true"
aria-describedby="invalid-price"
{% endif -%}
/>
{% if !price_error.is_empty() -%}
<small id="invalid-price">{{ price_error }}</small>
{% endif -%}
</div>
<button class="btn primary">Add Stock</button>
</form>

@ -0,0 +1,16 @@
<form hx-post="/item/{{item_id}}/adjustment/sale" hx-target="this" hx-swap="outerHTML" >
<div>
<label for="amount">Amount</label>
<input id="amount" name="amount" type="number" step="0.01" required
{% if !amount_error.is_empty() -%}
aria-invalid="true"
aria-describedby="invalid-amount"
{% endif -%}
/>
{% if !amount_error.is_empty() -%}
<small id="invalid-amount">{{ amount_error }}</small>
{% endif -%}
</div>
<button class="btn primary">Record Sale</button>
</form>

@ -1,12 +0,0 @@
<form hx-post="/item/{{item_id}}/adjustment/new" hx-target="this" hx-swap="outerHTML" >
<div>
<label for="amount">Amount</label>
<input id="amount" name="amount" type="number" >
</div>
<div>
<label for="reason">Reason</label>
<input id="reason" name="reason" type="text" >
</div>
<button class="btn primary">Add Adjustment</button>
</form>

@ -11,7 +11,10 @@
<div hx-get="/item/{{item_id}}/adjustments" hx-trigger="load" hx-swap="outerHTML">
</div>
<div hx-get="/item/{{item_id}}/adjustment/new" hx-trigger="load" hx-swap="outerHTML">
<div hx-get="/item/{{item_id}}/adjustment/sale" hx-trigger="load" hx-swap="outerHTML">
</div>
<div hx-get="/item/{{item_id}}/adjustment/new-stock" hx-trigger="load" hx-swap="outerHTML">
</div>
{% endblock %}
Loading…
Cancel
Save

Powered by TurnKey Linux.