parent
a22a052c99
commit
9124e66e28
@ -1,229 +0,0 @@
|
||||
use askama::Template;
|
||||
use askama_axum::Response;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::{debug_handler, Form};
|
||||
use axum::response::IntoResponse;
|
||||
use axum_htmx::{HxEvent, HxResponseTrigger};
|
||||
use oauth2::http;
|
||||
use serde::Deserialize;
|
||||
use sqlx::SqlitePool;
|
||||
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::{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::{dollars_string_to_int_cents, int_cents_to_dollars_string};
|
||||
use crate::util::formatting::format_either;
|
||||
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "adjustments-table-fragment.html")]
|
||||
struct AdjustmentTableTemplate {
|
||||
item_id: i64,
|
||||
adjustments: Vec<AdjustmentDisplayItem>,
|
||||
}
|
||||
|
||||
pub async fn get_adjustments_for_item(State(db): State<SqlitePool>,
|
||||
Path(id): Path<i64>,
|
||||
user: SessionUser
|
||||
) -> Result<Response, AppError> {
|
||||
|
||||
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_db_item(x, timezone, allow_fractional))
|
||||
.collect();
|
||||
|
||||
Ok(AdjustmentTableTemplate { item_id: id, adjustments }.into_response())
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "adjustment-sale-form.html")]
|
||||
pub struct AdjustmentSaleFormTemplate {
|
||||
pub item_id: i64,
|
||||
pub amount_error: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AdjustmentSaleFormData {
|
||||
pub amount: f64,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn adjustment_sale_form_post(State(db): State<SqlitePool>,
|
||||
Path(id): Path<i64>,
|
||||
user: SessionUser,
|
||||
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();
|
||||
|
||||
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 {
|
||||
""
|
||||
};
|
||||
|
||||
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,
|
||||
Some(unit_price),
|
||||
DbAdjustmentReason::NewStock).await?;
|
||||
|
||||
|
||||
Ok((trigger_events, AdjustmentNewStockFormTemplate { item_id: id,
|
||||
amount_error: "", price_error: "" }.into_response()).into_response())
|
||||
}
|
||||
|
||||
pub async fn adjustment_new_stock_form_get(
|
||||
Path(id): Path<i64>
|
||||
) -> Result<Response, AppError> {
|
||||
Ok(AdjustmentNewStockFormTemplate { item_id: id,
|
||||
amount_error: "", price_error: "" }.into_response())
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AdjustmentDisplayItem {
|
||||
pub date: String,
|
||||
pub amount: String,
|
||||
pub unit_value: String,
|
||||
pub tally: String,
|
||||
pub tally_value: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
|
||||
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 = 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 = format!("{:.*}", precision, db_entry.tally);
|
||||
let tally_value = currency::int_cents_to_dollars_string(db_entry.value);
|
||||
let reason = db_entry.adjustment.reason.into();
|
||||
|
||||
Self {
|
||||
date,
|
||||
amount,
|
||||
unit_value,
|
||||
tally,
|
||||
tally_value,
|
||||
reason,
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
use anyhow::anyhow;
|
||||
use askama::Template;
|
||||
use askama_axum::IntoResponse;
|
||||
use axum::extract::{Path, State};
|
||||
use sqlx::SqlitePool;
|
||||
use axum::response::Response;
|
||||
use crate::app::adjustment::AdjustmentDisplayItem;
|
||||
use crate::db::adjustment::{get_item_adjustment_valuation_weighted_mean, sum_all_adjustments_for_item, DbAdjustmentWithValuation};
|
||||
use crate::db::inventory_item::{inventory_item_get_by_id_with_unit, inventory_item_get_unit_abbreviation, DbInventoryItemWithCount};
|
||||
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")]
|
||||
struct ItemTemplate {
|
||||
pub item_id: i64,
|
||||
pub item: ItemDisplayItem,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ItemDisplayItem {
|
||||
pub name: String,
|
||||
pub reorder_point: f64,
|
||||
pub allow_fractional_units: bool,
|
||||
pub active: bool,
|
||||
pub display_unit: String,
|
||||
pub display_unit_short: String,
|
||||
pub amount: String,
|
||||
}
|
||||
|
||||
impl From<DbInventoryItemWithCount> for ItemDisplayItem {
|
||||
fn from(item: DbInventoryItemWithCount) -> Self {
|
||||
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,
|
||||
allow_fractional_units: item.allow_fractional_units,
|
||||
display_unit: item.display_unit_str,
|
||||
display_unit_short: item.display_unit_abbreviation,
|
||||
active: item.active,
|
||||
amount,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn item(State(db): State<SqlitePool>,
|
||||
Path(id): Path<i64>,
|
||||
user: SessionUser
|
||||
) -> Result<Response, AppError> {
|
||||
let item: ItemDisplayItem = inventory_item_get_by_id_with_unit(&db, id).await?.into();
|
||||
|
||||
Ok(ItemTemplate { item_id: id, item }.into_response())
|
||||
}
|
||||
|
||||
|
||||
pub async fn item_count(State(db): State<SqlitePool>, Path(id): Path<i64>) -> Result<Response, AppError> {
|
||||
let count = sum_all_adjustments_for_item(&db, id).await?;
|
||||
|
||||
Ok(count.to_string().into_response())
|
||||
}
|
||||
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "item-stats-fragment.html")]
|
||||
struct ItemStatsTemplate {
|
||||
pub item_id: i64,
|
||||
pub item: ItemDisplayItem,
|
||||
pub value: String,
|
||||
}
|
||||
pub async fn item_stats(State(db): State<SqlitePool>, Path(id): Path<i64>) -> Result<Response, AppError> {
|
||||
//TODO This is pretty chatty with the database. Might could cut down the
|
||||
// number of queries
|
||||
let item: ItemDisplayItem = inventory_item_get_by_id_with_unit(&db, id).await?.into();
|
||||
|
||||
let value = get_item_adjustment_valuation_weighted_mean(&db, id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.last().ok_or_else(|| anyhow!("No value"))?
|
||||
.value;
|
||||
|
||||
let value = int_cents_to_dollars_string(value);
|
||||
|
||||
Ok(ItemStatsTemplate { item_id: id, item, value }.into_response())
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
pub mod table;
|
||||
pub mod negative;
|
||||
pub mod positive;
|
||||
@ -0,0 +1,114 @@
|
||||
use crate::db::adjustment::add_adjustment;
|
||||
use crate::db::adjustment::adjustment_reason::DbAdjustmentReason;
|
||||
use crate::db::inventory_item::does_inventory_item_allow_fractional_units;
|
||||
use crate::error::AppError;
|
||||
use crate::session::SessionUser;
|
||||
use askama::Template;
|
||||
use askama_axum::{IntoResponse, Response};
|
||||
use axum::extract::{Path, State};
|
||||
use axum::{debug_handler, Form};
|
||||
use axum_htmx::{HxEvent, HxResponseTrigger};
|
||||
use serde::Deserialize;
|
||||
use sqlx::SqlitePool;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "item/adjustment/negative-adjustment-form.html")]
|
||||
pub struct NegativeAdjustmentFormTemplate {
|
||||
pub item_id: i64,
|
||||
pub amount_error: &'static str,
|
||||
pub reason_error: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct NegativeAdjustmentFormData {
|
||||
pub amount: f64,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn negative_adjustment_form_post(
|
||||
State(db): State<SqlitePool>,
|
||||
Path(id): Path<i64>,
|
||||
user: SessionUser,
|
||||
mut form_data: Form<NegativeAdjustmentFormData>,
|
||||
) -> 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?;
|
||||
|
||||
let reason = form_data
|
||||
.reason
|
||||
.take()
|
||||
.and_then(|s| DbAdjustmentReason::try_from(s.as_str()).ok())
|
||||
.unwrap_or_else(|| DbAdjustmentReason::Unknown);
|
||||
|
||||
let amount_error = if adjustment_amount == 0.0 {
|
||||
"Please input a non-zero amount"
|
||||
} else if !(fractional_units_allowed || adjustment_amount.fract() == 0.0) {
|
||||
"Please input a whole number"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let reason_error = if reason == DbAdjustmentReason::Unknown {
|
||||
"Unknown adjustment reason"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
if !(amount_error.is_empty() && reason_error.is_empty()) {
|
||||
return Ok(NegativeAdjustmentFormTemplate {
|
||||
item_id: id,
|
||||
amount_error,
|
||||
reason_error,
|
||||
}
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let trigger_events =
|
||||
HxResponseTrigger::normal(std::iter::once(HxEvent::from("new-adjustment")));
|
||||
|
||||
let timestamp = chrono::Utc::now();
|
||||
|
||||
info!(
|
||||
"Add adjustment form: (Amount {}, Reason {:?}, for user {}",
|
||||
form_data.amount, reason, user.name
|
||||
);
|
||||
|
||||
let _new_id = add_adjustment(
|
||||
&db,
|
||||
id,
|
||||
user.id,
|
||||
timestamp,
|
||||
timestamp,
|
||||
adjustment_amount,
|
||||
None,
|
||||
reason,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((
|
||||
trigger_events,
|
||||
NegativeAdjustmentFormTemplate {
|
||||
item_id: id,
|
||||
amount_error: "",
|
||||
reason_error: "",
|
||||
}
|
||||
.into_response(),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn negative_adjustment_form_get(Path(id): Path<i64>) -> Result<Response, AppError> {
|
||||
Ok(NegativeAdjustmentFormTemplate {
|
||||
item_id: id,
|
||||
amount_error: "",
|
||||
reason_error: "",
|
||||
}
|
||||
.into_response())
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
use crate::db::adjustment::add_adjustment;
|
||||
use crate::db::adjustment::adjustment_reason::DbAdjustmentReason;
|
||||
use crate::db::inventory_item::does_inventory_item_allow_fractional_units;
|
||||
use crate::error::AppError;
|
||||
use crate::session::SessionUser;
|
||||
use crate::util::currency::dollars_string_to_int_cents;
|
||||
use askama::Template;
|
||||
use askama_axum::{IntoResponse, Response};
|
||||
use axum::extract::{Path, State};
|
||||
use axum::{debug_handler, Form};
|
||||
use axum_htmx::{HxEvent, HxResponseTrigger};
|
||||
use serde::Deserialize;
|
||||
use sqlx::SqlitePool;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "item/adjustment/positive-adjustment-form.html")]
|
||||
pub struct PositiveAdjustmentFormTemplate {
|
||||
pub item_id: i64,
|
||||
pub amount_error: &'static str,
|
||||
pub price_error: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct PositiveAdjustmentFormData {
|
||||
pub amount: f64,
|
||||
pub price: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn positive_adjustment_form_post(
|
||||
State(db): State<SqlitePool>,
|
||||
Path(id): Path<i64>,
|
||||
user: SessionUser,
|
||||
form_data: Form<PositiveAdjustmentFormData>,
|
||||
) -> 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 {
|
||||
""
|
||||
};
|
||||
|
||||
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(PositiveAdjustmentFormTemplate {
|
||||
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,
|
||||
Some(unit_price),
|
||||
DbAdjustmentReason::NewStock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((
|
||||
trigger_events,
|
||||
PositiveAdjustmentFormTemplate {
|
||||
item_id: id,
|
||||
amount_error: "",
|
||||
price_error: "",
|
||||
}
|
||||
.into_response(),
|
||||
)
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn positive_adjustment_form_get(Path(id): Path<i64>) -> Result<Response, AppError> {
|
||||
Ok(PositiveAdjustmentFormTemplate {
|
||||
item_id: id,
|
||||
amount_error: "",
|
||||
price_error: "",
|
||||
}
|
||||
.into_response())
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
use crate::db::adjustment::{
|
||||
get_item_adjustment_valuation_weighted_mean, DbAdjustmentWithValuation,
|
||||
};
|
||||
use crate::db::inventory_item::does_inventory_item_allow_fractional_units;
|
||||
use crate::error::AppError;
|
||||
use crate::session::SessionUser;
|
||||
use crate::util::currency;
|
||||
use askama::Template;
|
||||
use askama_axum::{IntoResponse, Response};
|
||||
use axum::extract::{Path, State};
|
||||
use chrono::FixedOffset;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "item/adjustment/adjustment-table.html")]
|
||||
struct AdjustmentTableTemplate {
|
||||
item_id: i64,
|
||||
adjustments: Vec<AdjustmentDisplayItem>,
|
||||
}
|
||||
|
||||
pub async fn get_adjustments_table(
|
||||
State(db): State<SqlitePool>,
|
||||
Path(id): Path<i64>,
|
||||
user: SessionUser,
|
||||
) -> Result<Response, AppError> {
|
||||
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_db_item(x, timezone, allow_fractional))
|
||||
.collect();
|
||||
|
||||
Ok(AdjustmentTableTemplate {
|
||||
item_id: id,
|
||||
adjustments,
|
||||
}
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AdjustmentDisplayItem {
|
||||
pub date: String,
|
||||
pub amount: String,
|
||||
pub unit_value: String,
|
||||
pub tally: String,
|
||||
pub tally_value: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
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 = 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 = format!("{:.*}", precision, db_entry.tally);
|
||||
let tally_value = currency::int_cents_to_dollars_string(db_entry.value);
|
||||
let reason = db_entry.adjustment.reason.into();
|
||||
|
||||
Self {
|
||||
date,
|
||||
amount,
|
||||
unit_value,
|
||||
tally,
|
||||
tally_value,
|
||||
reason,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
use crate::db::adjustment::sum_all_adjustments_for_item;
|
||||
use crate::error::AppError;
|
||||
use askama_axum::{IntoResponse, Response};
|
||||
use axum::debug_handler;
|
||||
use axum::extract::{Path, State};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn item_count(
|
||||
State(db): State<SqlitePool>,
|
||||
Path(id): Path<i64>,
|
||||
) -> Result<Response, AppError> {
|
||||
let count = sum_all_adjustments_for_item(&db, id).await?;
|
||||
|
||||
Ok(count.to_string().into_response())
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
use crate::db::inventory_item::{inventory_item_get_by_id_with_unit, DbInventoryItemWithCount};
|
||||
use crate::error::AppError;
|
||||
use askama::Template;
|
||||
use askama_axum::{IntoResponse, Response};
|
||||
use axum::debug_handler;
|
||||
use axum::extract::{Path, State};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "item/item.html")]
|
||||
struct ItemTemplate {
|
||||
pub item_id: i64,
|
||||
pub item: ItemDisplayItem,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn item(State(db): State<SqlitePool>, Path(id): Path<i64>) -> Result<Response, AppError> {
|
||||
let item: ItemDisplayItem = inventory_item_get_by_id_with_unit(&db, id).await?.into();
|
||||
|
||||
Ok(ItemTemplate { item_id: id, item }.into_response())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ItemDisplayItem {
|
||||
pub name: String,
|
||||
pub reorder_point: f64,
|
||||
pub allow_fractional_units: bool,
|
||||
pub active: bool,
|
||||
pub display_unit: String,
|
||||
pub display_unit_short: String,
|
||||
pub amount: String,
|
||||
}
|
||||
|
||||
impl From<DbInventoryItemWithCount> for ItemDisplayItem {
|
||||
fn from(item: DbInventoryItemWithCount) -> Self {
|
||||
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,
|
||||
allow_fractional_units: item.allow_fractional_units,
|
||||
display_unit: item.display_unit_str,
|
||||
display_unit_short: item.display_unit_abbreviation,
|
||||
active: item.active,
|
||||
amount,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
pub mod adjustment;
|
||||
pub mod item;
|
||||
pub mod stats;
|
||||
pub mod count;
|
||||
@ -0,0 +1,36 @@
|
||||
use askama::Template;
|
||||
use axum::extract::{Path, State};
|
||||
use sqlx::SqlitePool;
|
||||
use askama_axum::{IntoResponse, Response};
|
||||
use anyhow::anyhow;
|
||||
use axum::debug_handler;
|
||||
use crate::app::item::item::ItemDisplayItem;
|
||||
use crate::db::adjustment::get_item_adjustment_valuation_weighted_mean;
|
||||
use crate::db::inventory_item::inventory_item_get_by_id_with_unit;
|
||||
use crate::error::AppError;
|
||||
use crate::util::currency::int_cents_to_dollars_string;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "item/stats-fragment.html")]
|
||||
struct ItemStatsTemplate {
|
||||
pub item_id: i64,
|
||||
pub item: ItemDisplayItem,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[debug_handler]
|
||||
pub async fn item_stats(State(db): State<SqlitePool>, Path(id): Path<i64>) -> Result<Response, AppError> {
|
||||
//TODO This is pretty chatty with the database. Might could cut down the
|
||||
// number of queries
|
||||
let item: ItemDisplayItem = inventory_item_get_by_id_with_unit(&db, id).await?.into();
|
||||
|
||||
let value = get_item_adjustment_valuation_weighted_mean(&db, id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.last().ok_or_else(|| anyhow!("No value"))?
|
||||
.value;
|
||||
|
||||
let value = int_cents_to_dollars_string(value);
|
||||
|
||||
Ok(ItemStatsTemplate { item_id: id, item, value }.into_response())
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
use crate::app::{catalog, history, home, item, overview, reports, upload};
|
||||
use crate::session::SessionUser;
|
||||
use axum::extract::FromRef;
|
||||
use axum::middleware::from_extractor;
|
||||
use axum::routing::{get, post};
|
||||
use axum::Router;
|
||||
use axum_htmx::AutoVaryLayer;
|
||||
use oauth2::basic::BasicClient;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
pub fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(home::home))
|
||||
.route("/index.html", get(home::home))
|
||||
.route("/home", get(home::home))
|
||||
.route("/home/search", get(home::home))
|
||||
.route("/catalog", get(catalog::catalog))
|
||||
.route("/catalog/", get(catalog::catalog))
|
||||
.route("/item/:item_id", get(item::item::item))
|
||||
.route("/item/:item_id/", get(item::item::item))
|
||||
.route("/item/:item_id/count", get(item::count::item_count))
|
||||
.route("/item/:item_id/stats", get(item::stats::item_stats))
|
||||
.route(
|
||||
"/item/:item_id/adjustments",
|
||||
get(item::adjustment::table::get_adjustments_table),
|
||||
)
|
||||
.route(
|
||||
"/item/:item_id/adjustment/negative",
|
||||
get(item::adjustment::negative::negative_adjustment_form_get)
|
||||
.post(item::adjustment::negative::negative_adjustment_form_post),
|
||||
)
|
||||
.route(
|
||||
"/item/:item_id/adjustment/positive",
|
||||
get(item::adjustment::positive::positive_adjustment_form_get)
|
||||
.post(item::adjustment::positive::positive_adjustment_form_post),
|
||||
)
|
||||
.route("/upload", get(upload::index::upload_index_handler))
|
||||
.route("/upload/catalog", post(upload::catalog::catalog_import))
|
||||
.route("/overview", get(overview::overview_handler))
|
||||
.route("/reports", get(reports::reports_handler))
|
||||
.route("/history", get(history::history_log_handler))
|
||||
// Ensure that all routes here require an authenticated user
|
||||
// whether explicitly asked or not
|
||||
.route_layer(from_extractor::<SessionUser>())
|
||||
.layer(AutoVaryLayer)
|
||||
}
|
||||
|
||||
// App state. Pretty basic stuff. Gets passed around by the server to the handlers and whatnot
|
||||
// Use in a handler with the state enum:
|
||||
// async fn handler(State(my_app_state): State<AppState>)
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub db: SqlitePool,
|
||||
pub oauth_client: BasicClient,
|
||||
}
|
||||
|
||||
// Axum extractors for app state. These allow the handler to just use
|
||||
// pieces of the App state
|
||||
// async fn handler(State(my_db): State<SqlitePool>)
|
||||
impl FromRef<AppState> for SqlitePool {
|
||||
fn from_ref(input: &AppState) -> Self {
|
||||
input.db.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRef<AppState> for BasicClient {
|
||||
fn from_ref(input: &AppState) -> Self {
|
||||
input.oauth_client.clone()
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
|
||||
<form hx-post="/item/{{item_id}}/adjustment/sale" hx-target="this" hx-swap="outerHTML" >
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<!--<label for="amount" class="form-label">Amount</label>-->
|
||||
<input type="number"
|
||||
id="amount"
|
||||
name="amount"
|
||||
step="0.01"
|
||||
class="form-control"
|
||||
placeholder="Amount"
|
||||
aria-label="amount"
|
||||
required
|
||||
{% if !amount_error.is_empty() -%}
|
||||
aria-invalid="true"
|
||||
aria-describedby="invalid-amount"
|
||||
{% endif -%}
|
||||
/>
|
||||
{% if !amount_error.is_empty() -%}
|
||||
<small id="invalid-amount" class="invalid-feedback">{{ amount_error }}</small>
|
||||
{% endif -%}
|
||||
</div>
|
||||
<div class="col">
|
||||
<button class="btn btn-primary">Record Sale</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@ -0,0 +1,65 @@
|
||||
<form hx-post="/item/{{item_id}}/adjustment/negative"
|
||||
hx-target="this" hx-swap="outerHTML"
|
||||
x-ref="formNegativeAdjustment"
|
||||
x-data="{ reason: 'Sale' }">
|
||||
<input id="reason" name="reason" type="hidden" x-model="reason"/>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<!--<label for="amount" class="form-label">Amount</label>-->
|
||||
<input type="number"
|
||||
id="amount"
|
||||
name="amount"
|
||||
step="0.01"
|
||||
class="form-control"
|
||||
placeholder="Amount"
|
||||
aria-label="amount"
|
||||
required
|
||||
{% if !amount_error.is_empty() -%}
|
||||
aria-invalid="true"
|
||||
aria-describedby="invalid-amount"
|
||||
{% endif -%}
|
||||
/>
|
||||
{% if !amount_error.is_empty() -%}
|
||||
<small id="invalid-amount" class="invalid-feedback">{{ amount_error }}</small>
|
||||
{% endif -%}
|
||||
</div>
|
||||
<div class="col">
|
||||
<button class="btn btn-primary" x-text="reason">Sale</button>
|
||||
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<span class="visually-hidden">Other</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item"
|
||||
@click="reason = 'Sale'">
|
||||
Sale
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item"
|
||||
@click="reason = 'Destruction'">
|
||||
Destruction
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item"
|
||||
@click="reason = 'Expiration'">
|
||||
Expiration
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item"
|
||||
@click="reason = 'Theft'">
|
||||
Theft
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@ -1,5 +1,5 @@
|
||||
|
||||
<form hx-post="/item/{{item_id}}/adjustment/new-stock" hx-target="this" hx-swap="outerHTML" >
|
||||
<form hx-post="/item/{{item_id}}/adjustment/positive" hx-target="this" hx-swap="outerHTML" >
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input type="number"
|
||||
Loading…
Reference in new issue