Add framework for adding adjustments

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

7
Cargo.lock generated

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -1164,6 +1164,7 @@ dependencies = [
"serde",
"sqlx",
"tokio",
"tokio-stream",
"tower 0.5.1",
"tower-http",
"tower-sessions",
@ -2657,9 +2658,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.16"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
dependencies = [
"futures-core",
"pin-project-lite",

@ -25,6 +25,7 @@ axum-extra = "0.9.4"
csv = "1.3.1"
ron = "0.8.1"
chrono = { version = "0.4.38", features = ["serde"] }
tokio-stream = "0.1.17"
[dev-dependencies]
httpc-test = "0.1.10"

@ -0,0 +1,129 @@
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::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;
#[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 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)))
.collect();
Ok(AdjustmentTableTemplate { item_id: id, adjustments }.into_response())
}
#[derive(Template)]
#[template(path = "adjustments-add-form.html")]
pub struct AdjustmentAddFormTemplate {
pub item_id: i64,
}
#[derive(Deserialize, Debug)]
pub struct AddAdjustmentFormData {
pub amount: f64,
pub reason: Option<String>,
}
#[debug_handler]
pub async fn adjustment_add_form_post(State(db): State<SqlitePool>,
Path(id): Path<i64>,
user: SessionUser,
form_data: Form<AddAdjustmentFormData>
) -> Result<Response, AppError> {
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
} else {
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, AdjustmentAddFormTemplate { item_id: id }.into_response()).into_response())
}
pub async fn adjustment_add_form_get(
Path(id): Path<i64>
) -> Result<Response, AppError> {
Ok(AdjustmentAddFormTemplate { item_id: id }.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 From<(DbAdjustmentWithValuation, FixedOffset)> for AdjustmentDisplayItem {
fn from(item: (DbAdjustmentWithValuation, FixedOffset)) -> Self {
let db_entry = item.0;
let timezone = item.1;
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 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_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,5 +1,5 @@
use crate::app::common::query_args::datetime_range::DatetimeRangeQueryArgs;
use crate::db::adjustment::{get_adjustments_target_date_range, DbAdjustment, DbAdjustmentWithUserAndItem};
use crate::db::adjustment::{get_adjustments_target_date_range, DbAdjustment};
use crate::error::{AppError, QueryExtractor};
use crate::session::SessionUser;
use crate::util::time::{tz_offset_to_string, LocalTimestampRange, UtcTimestampRange};
@ -12,6 +12,7 @@ use chrono::prelude::*;
use serde::Deserialize;
use sqlx::SqlitePool;
use tracing::info;
use crate::db::adjustment::adjustment_joins::DbAdjustmentWithUserAndItem;
use crate::util::currency;
#[derive(Template)]

@ -3,19 +3,52 @@ use askama_axum::IntoResponse;
use axum::extract::{Path, State};
use sqlx::SqlitePool;
use axum::response::Response;
use crate::db::inventory_item::{inventory_item_get_by_id, sum_all_adjustments_for_item, DbInventoryItem};
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, DbInventoryItemWithCount};
use crate::error::AppError;
use crate::session::SessionUser;
use crate::util::currency::int_cents_to_dollars_string;
#[derive(Template)]
#[template(path = "item.html")]
struct ItemTemplate {
item: DbInventoryItem
pub item_id: i64,
pub item: ItemDisplayItem,
}
pub async fn item(State(db): State<SqlitePool>, Path(id): Path<i64>) -> Result<Response, AppError> {
let item = inventory_item_get_by_id(&db, id).await?;
#[derive(Clone, Debug)]
struct ItemDisplayItem {
pub name: String,
pub reorder_point: f64,
pub allow_fractional_units: bool,
pub display_unit: String,
pub display_unit_short: String,
pub amount: String,
}
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);
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,
amount,
}
}
}
pub async fn item(State(db): State<SqlitePool>,
Path(id): Path<i64>,
user: SessionUser
) -> Result<Response, AppError> {
let mut item: ItemDisplayItem = inventory_item_get_by_id_with_unit(&db, id).await?.into();
Ok(ItemTemplate { item }.into_response())
Ok(ItemTemplate { item_id: id, item }.into_response())
}

@ -16,6 +16,7 @@ mod overview;
mod reports;
mod history;
pub mod common;
pub mod adjustment;
pub fn routes() -> Router<AppState> {
Router::new()
@ -28,6 +29,9 @@ pub fn routes() -> Router<AppState> {
.route("/item/:item_id", get(item::item))
.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("/upload", get(upload::index::upload_index_handler))
.route("/upload/catalog", post(upload::catalog::catalog_import))
.route("/overview", get(overview::overview_handler))

@ -1,206 +0,0 @@
use serde::Serialize;
use anyhow::Result;
use chrono::{DateTime, Utc};
use sqlx::SqlitePool;
use tracing::error;
#[derive(Clone, Debug, Serialize)]
#[derive(sqlx::FromRow)]
pub struct DbAdjustment {
pub id: i64,
pub item: i64,
pub user: i64,
pub create_date: DateTime<Utc>,
pub target_date: DateTime<Utc>,
pub amount: f64,
pub unit_price: Option<i64>,
#[sqlx(try_from = "i64")]
pub reason: DbAdjustmentReason,
}
pub async fn add_new_stock_adjustment(db: &SqlitePool, item: i64, user: i64,
create_date: DateTime<Utc>, target_date: DateTime<Utc>,
amount: f64, unit_price: i64)
-> Result<i64> {
let reason: i64 = DbAdjustmentReason::NewStock.into();
let res = sqlx::query!(
r#"
INSERT INTO Adjustment (
item,
user,
create_date,
target_date,
amount,
reason,
unit_price)
VALUES (?, ?, ?, ?, ?, ?, ?)
"#,
item, user, create_date, target_date, amount, reason, unit_price
).execute(db).await?;
let new_id = res.last_insert_rowid();
Ok(new_id)
}
pub async fn get_adjustments_target_date_range(
db: &SqlitePool, start_date: DateTime<Utc>, end_date: DateTime<Utc>
) -> Result<Vec<DbAdjustment>> {
sqlx::query_as::<_, DbAdjustment>(r#"
SELECT id, item, user,
create_date,
target_date,
amount,
reason,
unit_price
FROM Adjustment
WHERE target_date >= ? AND target_date <= ?
"#,)
.bind(start_date)
.bind(end_date)
.fetch_all(db)
.await.map_err(Into::into)
}
#[derive(Clone, Debug, Serialize)]
#[derive(sqlx::FromRow)]
pub struct DbAdjustmentWithUserAndItem {
pub id: i64,
pub item: i64,
pub item_name: String,
pub item_unit: String,
pub item_unit_abbreviation: String,
pub user: i64,
pub user_name: String,
pub create_date: DateTime<Utc>,
pub target_date: DateTime<Utc>,
pub amount: f64,
pub unit_price: Option<i64>,
#[sqlx(try_from = "i64")]
pub reason: DbAdjustmentReason,
}
impl DbAdjustmentWithUserAndItem {
pub async fn query_by_date_range(db: &SqlitePool,
start_date: DateTime<Utc>,
end_date: DateTime<Utc>,
page_size: i64,
page_num: i64) -> Result<Vec<Self>> {
let offset = page_size * page_num;
sqlx::query_as::<_, DbAdjustmentWithUserAndItem>(r#"
SELECT
adj.id as id,
adj.item as item,
item.name as item_name,
unit.unit as item_unit,
unit.abbreviation as item_unit_abbreviation,
adj.user as user,
user.name as user_name,
adj.create_date as create_date,
adj.target_date as target_date,
adj.amount as amount,
adj.unit_price as unit_price,
adj.reason as reason
FROM Adjustment as adj
JOIN User as user on adj.user = user.id
JOIN InventoryItem as item on adj.item = item.id
JOIN DisplayUnit as unit on item.display_unit = unit.id
WHERE adj.target_date >= ? AND adj.target_date <= ?
LIMIT ? OFFSET ?
"#,)
.bind(start_date)
.bind(end_date)
.bind(page_size)
.bind(offset)
.fetch_all(db)
.await.map_err(Into::into)
}
}
impl Into<DbAdjustment> for DbAdjustmentWithUserAndItem {
fn into(self) -> DbAdjustment {
DbAdjustment {
id: self.id,
item: self.item,
user: self.user,
create_date: self.create_date,
target_date: self.target_date,
amount: self.amount,
unit_price: self.unit_price,
reason: self.reason,
}
}
}
#[derive(Debug, Clone, Copy, Serialize)]
pub enum DbAdjustmentReason {
Unknown,
Sale,
Destruction,
Expiration,
Theft,
NewStock,
}
impl Into<i64> for DbAdjustmentReason {
fn into(self) -> i64 {
match self {
Self::Unknown => 0,
Self::Sale => 10,
Self::Destruction => 20,
Self::Expiration => 25,
Self::Theft => 30,
Self::NewStock => 50,
}
}
}
impl From<i64> for DbAdjustmentReason {
fn from(item: i64) -> Self {
match item {
0 => Self::Unknown,
10 => Self::Sale,
20 => Self::Destruction,
25 => Self::Expiration,
30 => Self::Theft,
50 => Self::NewStock,
_ => {
error!("unknown negative adjustment reason value: {}", item);
Self::Unknown
}
}
}
}
impl TryFrom<&str> for DbAdjustmentReason {
type Error = anyhow::Error;
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
match value {
"unknown" => Ok(Self::Unknown),
"sale" => Ok(Self::Sale),
"destruction" => Ok(Self::Destruction),
"expiration" => Ok(Self::Expiration),
"theft" => Ok(Self::Theft),
"new-stock" => Ok(Self::NewStock),
_ => Err(anyhow::anyhow!("unknown negative adjustment reason"))
}
}
}
impl Into<String> for DbAdjustmentReason {
fn into(self) -> String {
match self {
Self::Unknown => { String::from("unknown") }
Self::Sale => { String::from("sale") }
Self::Destruction => { String::from("destruction") }
Self::Expiration => { String::from("expiration") }
Self::Theft => { String::from("theft") }
Self::NewStock => { String::from("new-stock") }
}
}
}

@ -0,0 +1,76 @@
use serde::Serialize;
use chrono::{DateTime, Utc};
use sqlx::SqlitePool;
use crate::db::adjustment::adjustment_reason::DbAdjustmentReason;
use crate::db::adjustment::DbAdjustment;
#[derive(Clone, Debug, Serialize)]
#[derive(sqlx::FromRow)]
pub struct DbAdjustmentWithUserAndItem {
pub id: i64,
pub item: i64,
pub item_name: String,
pub item_unit: String,
pub item_unit_abbreviation: String,
pub user: i64,
pub user_name: String,
pub create_date: DateTime<Utc>,
pub target_date: DateTime<Utc>,
pub amount: f64,
pub unit_price: Option<i64>,
#[sqlx(try_from = "i64")]
pub reason: DbAdjustmentReason,
}
impl DbAdjustmentWithUserAndItem {
pub async fn query_by_date_range(db: &SqlitePool,
start_date: DateTime<Utc>,
end_date: DateTime<Utc>,
page_size: i64,
page_num: i64) -> anyhow::Result<Vec<Self>> {
let offset = page_size * page_num;
sqlx::query_as::<_, DbAdjustmentWithUserAndItem>(r#"
SELECT
adj.id as id,
adj.item as item,
item.name as item_name,
unit.unit as item_unit,
unit.abbreviation as item_unit_abbreviation,
adj.user as user,
user.name as user_name,
adj.create_date as create_date,
adj.target_date as target_date,
adj.amount as amount,
adj.unit_price as unit_price,
adj.reason as reason
FROM Adjustment as adj
JOIN User as user on adj.user = user.id
JOIN InventoryItem as item on adj.item = item.id
JOIN DisplayUnit as unit on item.display_unit = unit.id
WHERE adj.target_date >= ? AND adj.target_date <= ?
LIMIT ? OFFSET ?
"#,)
.bind(start_date)
.bind(end_date)
.bind(page_size)
.bind(offset)
.fetch_all(db)
.await.map_err(Into::into)
}
}
impl Into<DbAdjustment> for DbAdjustmentWithUserAndItem {
fn into(self) -> DbAdjustment {
DbAdjustment {
id: self.id,
item: self.item,
user: self.user,
create_date: self.create_date,
target_date: self.target_date,
amount: self.amount,
unit_price: self.unit_price,
reason: self.reason,
}
}
}

@ -0,0 +1,71 @@
use tracing::error;
use serde::Serialize;
#[derive(Debug, Clone, Copy, Serialize)]
pub enum DbAdjustmentReason {
Unknown,
Sale,
Destruction,
Expiration,
Theft,
NewStock,
}
impl Into<i64> for DbAdjustmentReason {
fn into(self) -> i64 {
match self {
Self::Unknown => 0,
Self::Sale => 10,
Self::Destruction => 20,
Self::Expiration => 25,
Self::Theft => 30,
Self::NewStock => 50,
}
}
}
impl From<i64> for DbAdjustmentReason {
fn from(item: i64) -> Self {
match item {
0 => Self::Unknown,
10 => Self::Sale,
20 => Self::Destruction,
25 => Self::Expiration,
30 => Self::Theft,
50 => Self::NewStock,
_ => {
error!("unknown negative adjustment reason value: {}", item);
Self::Unknown
}
}
}
}
impl TryFrom<&str> for DbAdjustmentReason {
type Error = anyhow::Error;
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
match value {
"unknown" => Ok(Self::Unknown),
"sale" => Ok(Self::Sale),
"destruction" => Ok(Self::Destruction),
"expiration" => Ok(Self::Expiration),
"theft" => Ok(Self::Theft),
"new-stock" => Ok(Self::NewStock),
_ => Err(anyhow::anyhow!("unknown negative adjustment reason"))
}
}
}
impl Into<String> for DbAdjustmentReason {
fn into(self) -> String {
match self {
Self::Unknown => { String::from("unknown") }
Self::Sale => { String::from("sale") }
Self::Destruction => { String::from("destruction") }
Self::Expiration => { String::from("expiration") }
Self::Theft => { String::from("theft") }
Self::NewStock => { String::from("new-stock") }
}
}
}

@ -0,0 +1,165 @@
pub mod adjustment_reason;
pub mod adjustment_joins;
use serde::Serialize;
use anyhow::Result;
use chrono::{DateTime, Utc};
use sqlx::SqlitePool;
use adjustment_reason::DbAdjustmentReason;
use tokio_stream::StreamExt;
#[derive(Clone, Debug, Serialize)]
#[derive(sqlx::FromRow)]
pub struct DbAdjustment {
pub id: i64,
pub item: i64,
pub user: i64,
pub create_date: DateTime<Utc>,
pub target_date: DateTime<Utc>,
pub amount: f64,
pub unit_price: Option<i64>,
#[sqlx(try_from = "i64")]
pub reason: DbAdjustmentReason,
}
pub async fn add_adjustment_new_stock(db: &SqlitePool, item: i64, user: i64,
create_date: DateTime<Utc>, target_date: DateTime<Utc>,
amount: f64, unit_price: i64)
-> Result<i64> {
add_adjustment(db, item, user, create_date, target_date, amount, Some(unit_price),
DbAdjustmentReason::NewStock).await
}
pub async fn add_adjustment(db: &SqlitePool, item: i64, user: i64,
create_date: DateTime<Utc>, target_date: DateTime<Utc>,
amount: f64, unit_price: Option<i64>, reason: DbAdjustmentReason)
-> Result<i64> {
let reason: i64 = reason.into();
let res = sqlx::query(
r#"
INSERT INTO Adjustment (
item,
user,
create_date,
target_date,
amount,
reason,
unit_price)
VALUES (?, ?, ?, ?, ?, ?, ?)
"#)
.bind(item)
.bind(user)
.bind(create_date)
.bind(target_date)
.bind(amount)
.bind(reason)
.bind(unit_price)
.execute(db).await?;
let new_id = res.last_insert_rowid();
Ok(new_id)
}
pub async fn get_adjustments_target_date_range(
db: &SqlitePool, start_date: DateTime<Utc>, end_date: DateTime<Utc>
) -> Result<Vec<DbAdjustment>> {
sqlx::query_as::<_, DbAdjustment>(r#"
SELECT id, item, user,
create_date,
target_date,
amount,
reason,
unit_price
FROM Adjustment
WHERE target_date >= ? AND target_date <= ?
"#,)
.bind(start_date)
.bind(end_date)
.fetch_all(db)
.await.map_err(Into::into)
}
#[derive(Clone, Debug, Serialize)]
#[derive(sqlx::FromRow)]
pub struct DbAdjustmentRunningTally {
#[sqlx(flatten)]
pub adjustment: DbAdjustment,
pub tally: f64,
}
pub async fn get_tallied_adjustments_for_item(
db: &SqlitePool, item: i64
) -> Result<Vec<DbAdjustmentRunningTally>> {
sqlx::query_as::<_, DbAdjustmentRunningTally>(r#"
SELECT id, item, user,
create_date,
target_date,
amount,
reason,
unit_price,
SUM(amount) OVER (partition by item order by target_date,id) as tally
FROM Adjustment
WHERE item = ?
"#,)
.bind(item)
.fetch_all(db)
.await.map_err(Into::into)
}
pub async fn sum_all_adjustments_for_item(db: &SqlitePool, id: i64) -> Result<f64> {
let res = sqlx::query!(
r#"
SELECT TOTAL(amount) as amt FROM Adjustment WHERE item = ?
"#,
id
)
.fetch_one(db).await?;
let amt: f64 = res.amt.unwrap_or_default();
Ok(amt)
}
#[derive(Clone, Debug, Serialize)]
#[derive(sqlx::FromRow)]
pub struct DbAdjustmentWithValuation {
pub adjustment: DbAdjustment,
pub tally: f64,
pub value: i64,
}
pub async fn get_item_adjustment_valuation_weighted_mean(db: &SqlitePool, item: i64) -> Result<Vec<DbAdjustmentWithValuation>> {
let mut stream = sqlx::query_as::<_, DbAdjustmentRunningTally>(r#"
SELECT id, item, user,
create_date,
target_date,
amount,
reason,
unit_price,
SUM(amount) OVER (partition by item order by target_date,id) as tally
FROM Adjustment
WHERE item = ?
ORDER BY target_date, id
"#,)
.bind(item)
.fetch(db);
//NOTE: I am positive that this could just be done with a pure query, but we'll just stick
// with the janky solution for now
let mut weighted_average = 0;
let mut vals = vec![];
while let Some(val) = stream.next().await.transpose()? {
let contrib = val.adjustment.amount * val.adjustment.unit_price.unwrap_or(weighted_average) as f64;
let current_value = weighted_average as f64 * (val.tally - val.adjustment.amount);
let new_value = (contrib + current_value) as i64;
weighted_average = new_value / val.tally as i64;
vals.push(DbAdjustmentWithValuation { adjustment: val.adjustment, tally: val.tally, value: new_value })
}
Ok(vals)
}

@ -82,21 +82,7 @@ pub async fn inventory_item_get_by_id(db: &SqlitePool, id: i64) -> Result<DbInve
.map_err(From::from)
}
pub async fn sum_all_adjustments_for_item(db: &SqlitePool, id: i64) -> Result<f64> {
let res = sqlx::query!(
r#"
SELECT TOTAL(amount) as amt FROM Adjustment WHERE item = ?
"#,
id
)
.fetch_one(db).await?;
let amt: f64 = res.amt.unwrap_or_default();
Ok(amt)
}
pub async fn add_inventory_item(db: &SqlitePool, name: &str, reorder_point: f64,
pub async fn add_inventory_item(db: &SqlitePool, name: &str, reorder_point: f64,
allow_fractional_units: bool, display_unit_abbreviation: &str
) -> Result<i64> {
let res = sqlx::query!(
@ -111,3 +97,40 @@ pub async fn add_inventory_item(db: &SqlitePool, name: &str, reorder_point: f64,
Ok(new_id)
}
#[derive(Debug, Serialize)]
#[derive(sqlx::FromRow)]
pub struct DbInventoryItemWithCount {
pub id: i64,
pub name: String,
pub reorder_point: f64,
pub allow_fractional_units: bool,
pub display_unit: i64,
pub display_unit_str: String,
pub display_unit_abbreviation: String,
pub amount: Option<f64>,
}
pub async fn inventory_item_get_by_id_with_unit(db: &SqlitePool, id: i64) -> Result<DbInventoryItemWithCount> {
sqlx::query_as!(
DbInventoryItemWithCount,
r#"
SELECT
item.id as id,
item.name as name,
item.reorder_point as reorder_point,
item.allow_fractional_units as allow_fractional_units,
item.display_unit as display_unit,
display_unit.unit as display_unit_str,
display_unit.abbreviation as display_unit_abbreviation,
(SELECT TOTAL(amount) as amt FROM Adjustment WHERE item = ?) as amount
FROM
InventoryItem as item
JOIN DisplayUnit as display_unit ON item.display_unit = display_unit.id
WHERE item.id = ?
"#,
id, id
)
.fetch_one(db).await
.map_err(From::from)
}

@ -4,7 +4,7 @@ use axum::body::Bytes;
use sqlx::SqlitePool;
use tracing::info;
use crate::db::inventory_item::add_inventory_item;
use crate::db::adjustment::add_new_stock_adjustment;
use crate::db::adjustment::add_adjustment_new_stock;
#[derive(Debug, serde::Deserialize)]
struct CatalogRecord {
@ -38,7 +38,7 @@ pub async fn ingest_catalog<T: std::io::Read>(mut reader: csv::Reader<T>, db: Sq
record.fractional,
&record.unit).await?;
let new_positive_adjustment = add_new_stock_adjustment(&db, new_entry_id,
let new_positive_adjustment = add_adjustment_new_stock(&db, new_entry_id,
user_id, timestamp, timestamp, record.quantity, record.unit_price).await?;
info!("Added new item: {}/{} - {}", new_entry_id, new_positive_adjustment, record.name);

@ -8,6 +8,7 @@ use axum::http::request::Parts;
use axum::response::Redirect;
use serde::{Deserialize, Serialize};
use std::result;
use chrono::FixedOffset;
use tokio::task::JoinHandle;
use tower_sessions::cookie::SameSite;
use tower_sessions::{ExpiredDeletion, Expiry, Session, SessionManagerLayer};
@ -101,4 +102,11 @@ where
Ok(user)
}
}
impl SessionUser {
pub fn get_timezone(&self) -> Result<FixedOffset> {
FixedOffset::east_opt(self.tz_offset)
.ok_or(anyhow::anyhow!("Invalid timezone"))
}
}

@ -0,0 +1,12 @@
<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>

@ -0,0 +1,25 @@
<table hx-get="/item/{{item_id}}/adjustments" hx-trigger="new-adjustment from:body" hx-swap="outerHTML">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Amount</th>
<th scope="col">Reason</th>
<th scope="col">Unit Value</th>
<th scope="col">Running Total</th>
<th scope="col">Running Value</th>
</tr>
</thead>
<tbody>
{% for item in adjustments %}
<tr>
<td>{{ item.date }}</td>
<td>{{ item.amount }}</td>
<td>{{ item.reason }}</td>
<td>{{ item.unit_value }}</td>
<td>{{ item.tally }}</td>
<td>{{ item.tally_value }}</td>
</tr>
{% endfor %}
</tbody>
</table>

@ -6,5 +6,12 @@
<h3>{{item.name}}</h3>
<p>Reorder at: {{item.reorder_point}}</p>
<p>Amount in stock: {{item.amount}} {{item.display_unit_short}}</p>
<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>
{% endblock %}
Loading…
Cancel
Save

Powered by TurnKey Linux.