parent
c66ae19ea8
commit
08194c5775
@ -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,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)
|
||||
}
|
||||
@ -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>
|
||||
Loading…
Reference in new issue