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