|
|
|
@ -11,11 +11,12 @@ use tracing::info;
|
|
|
|
use chrono::FixedOffset;
|
|
|
|
use chrono::FixedOffset;
|
|
|
|
use crate::db::adjustment::{add_adjustment, get_item_adjustment_valuation_weighted_mean, DbAdjustmentWithValuation};
|
|
|
|
use crate::db::adjustment::{add_adjustment, get_item_adjustment_valuation_weighted_mean, DbAdjustmentWithValuation};
|
|
|
|
use crate::db::adjustment::adjustment_reason::DbAdjustmentReason;
|
|
|
|
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::error::AppError;
|
|
|
|
use crate::session::SessionUser;
|
|
|
|
use crate::session::SessionUser;
|
|
|
|
use crate::util::currency;
|
|
|
|
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)]
|
|
|
|
#[derive(Template)]
|
|
|
|
@ -32,64 +33,161 @@ pub async fn get_adjustments_for_item(State(db): State<SqlitePool>,
|
|
|
|
|
|
|
|
|
|
|
|
let timezone = user.get_timezone()?;
|
|
|
|
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<DbAdjustmentWithValuation> = get_item_adjustment_valuation_weighted_mean(&db, id).await?;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let adjustments: Vec<AdjustmentDisplayItem> = adjustments.into_iter()
|
|
|
|
let adjustments: Vec<AdjustmentDisplayItem> = adjustments.into_iter()
|
|
|
|
.map(|x| AdjustmentDisplayItem::from((x, timezone)))
|
|
|
|
.map(|x| AdjustmentDisplayItem::from_db_item(x, timezone, allow_fractional))
|
|
|
|
.collect();
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
|
|
Ok(AdjustmentTableTemplate { item_id: id, adjustments }.into_response())
|
|
|
|
Ok(AdjustmentTableTemplate { item_id: id, adjustments }.into_response())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Template)]
|
|
|
|
#[derive(Template)]
|
|
|
|
#[template(path = "adjustments-add-form.html")]
|
|
|
|
#[template(path = "adjustment-sale-form.html")]
|
|
|
|
pub struct AdjustmentAddFormTemplate {
|
|
|
|
pub struct AdjustmentSaleFormTemplate {
|
|
|
|
pub item_id: i64,
|
|
|
|
pub item_id: i64,
|
|
|
|
|
|
|
|
pub amount_error: &'static str,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
pub struct AddAdjustmentFormData {
|
|
|
|
pub struct AdjustmentSaleFormData {
|
|
|
|
pub amount: f64,
|
|
|
|
pub amount: f64,
|
|
|
|
pub reason: Option<String>,
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[debug_handler]
|
|
|
|
#[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>,
|
|
|
|
Path(id): Path<i64>,
|
|
|
|
user: SessionUser,
|
|
|
|
user: SessionUser,
|
|
|
|
form_data: Form<AddAdjustmentFormData>
|
|
|
|
form_data: Form<AdjustmentSaleFormData>
|
|
|
|
) -> Result<Response, AppError> {
|
|
|
|
) -> 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(
|
|
|
|
let trigger_events = HxResponseTrigger::normal(
|
|
|
|
std::iter::once(HxEvent::from("new-adjustment"))
|
|
|
|
std::iter::once(HxEvent::from("new-adjustment"))
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
let timestamp = chrono::Utc::now();
|
|
|
|
let timestamp = chrono::Utc::now();
|
|
|
|
|
|
|
|
|
|
|
|
let adjustment_amount = if form_data.amount > 0.0 {
|
|
|
|
info!("Add adjustment form: {:?} for user {}", form_data, user.name);
|
|
|
|
-1.0 * form_data.amount
|
|
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
} 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);
|
|
|
|
info!("Add adjustment form: {:?} for user {}", form_data, user.name);
|
|
|
|
|
|
|
|
|
|
|
|
let _new_id = add_adjustment(&db, id, user.id,
|
|
|
|
let _new_id = add_adjustment(&db, id, user.id,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
|
timestamp,
|
|
|
|
adjustment_amount,
|
|
|
|
adjustment_amount,
|
|
|
|
None,
|
|
|
|
Some(unit_price),
|
|
|
|
DbAdjustmentReason::Sale).await?;
|
|
|
|
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>
|
|
|
|
Path(id): Path<i64>
|
|
|
|
) -> Result<Response, AppError> {
|
|
|
|
) -> 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,
|
|
|
|
pub reason: String,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl From<(DbAdjustmentWithValuation, FixedOffset)> for AdjustmentDisplayItem {
|
|
|
|
|
|
|
|
fn from(item: (DbAdjustmentWithValuation, FixedOffset)) -> Self {
|
|
|
|
impl AdjustmentDisplayItem {
|
|
|
|
let db_entry = item.0;
|
|
|
|
pub fn from_db_item(db_entry: DbAdjustmentWithValuation, timezone: FixedOffset
|
|
|
|
let timezone = item.1;
|
|
|
|
, allow_fractional: bool) -> Self {
|
|
|
|
|
|
|
|
let precision : usize = if allow_fractional { 2 } else { 0 };
|
|
|
|
let date = db_entry.adjustment.target_date.with_timezone(&timezone)
|
|
|
|
let date = db_entry.adjustment.target_date.with_timezone(&timezone)
|
|
|
|
.format("%Y-%m-%d %l:%M:%S %p").to_string();
|
|
|
|
.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(||
|
|
|
|
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 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 tally_value = currency::int_cents_to_dollars_string(db_entry.value);
|
|
|
|
let reason = db_entry.adjustment.reason.into();
|
|
|
|
let reason = db_entry.adjustment.reason.into();
|
|
|
|
|
|
|
|
|
|
|
|
@ -125,5 +224,6 @@ impl From<(DbAdjustmentWithValuation, FixedOffset)> for AdjustmentDisplayItem {
|
|
|
|
tally_value,
|
|
|
|
tally_value,
|
|
|
|
reason,
|
|
|
|
reason,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|