Better item stats (more reactive)

demo-mode
Wes Holland 1 year ago
parent beb9f2bc3d
commit c3d3eecf4d

@ -7,6 +7,9 @@
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:inventory-app.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
<driver-properties>
<property name="foreign_keys" value="true" />
</driver-properties>
</data-source>
</component>
</project>

@ -25,6 +25,11 @@ CREATE TABLE IF NOT EXISTS InventoryItem (
display_unit INTEGER NOT NULL DEFAULT 1,
reorder_point REAL NOT NULL,
allow_fractional_units BOOLEAN NOT NULL,
active BOOLEAN NOT NULL,
pims_id TEXT,
vetcove_id TEXT,
manufacturer_name TEXT,
manufacturer_id TEXT,
FOREIGN KEY(display_unit) REFERENCES DisplayUnit(id)
);
@ -65,3 +70,129 @@ INSERT INTO DisplayUnit (id, unit, abbreviation) VALUES
(1,'count', 'ct'),
(2,'milliliter', 'ml'),
(3,'milligram', 'mg');
CREATE TABLE IF NOT EXISTS SkuCovetrus
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuMWI
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuPatterson
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuMidwest
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuFirstVet
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuPennVet
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuAmatheon
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuVictor
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuVetcove
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuMillerVet
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuBoehringerIngelheim
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuMillerVet
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuZoetis
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuPharmasourceAH
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuNEAnimalHealth
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuDechra
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuMedline
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS SkuElanco
(
sku TEXT NOT NULL UNIQUE,
item INTEGER,
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);

@ -1,3 +1,4 @@
use anyhow::anyhow;
use askama::Template;
use askama_axum::IntoResponse;
use axum::extract::{Path, State};
@ -5,7 +6,7 @@ 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, DbInventoryItemWithCount};
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;
@ -23,6 +24,7 @@ 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,
@ -38,6 +40,7 @@ impl From<DbInventoryItemWithCount> for ItemDisplayItem {
allow_fractional_units: item.allow_fractional_units,
display_unit: item.display_unit_str,
display_unit_short: item.display_unit_abbreviation,
active: item.active,
amount,
}
}
@ -58,3 +61,30 @@ pub async fn item_count(State(db): State<SqlitePool>, Path(id): Path<i64>) -> Re
Ok(count.to_string().into_response())
}
#[derive(Template)]
#[template(path = "item-stats-fragment.html")]
struct ItemStatsTemplate {
pub item_id: i64,
pub amount: String,
pub unit_abbreviation: String,
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 amount = sum_all_adjustments_for_item(&db, id).await?.to_string();
let unit_abbreviation = inventory_item_get_unit_abbreviation(&db, id).await?;
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, amount, unit_abbreviation, value }.into_response())
}

@ -29,6 +29,7 @@ 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/stats", get(item::item_stats))
.route("/item/:item_id/adjustments", get(adjustment::get_adjustments_for_item))
.route("/item/:item_id/adjustment/sale", post(adjustment::adjustment_sale_form_post))
.route("/item/:item_id/adjustment/sale", get(adjustment::adjustment_sale_form_get))

@ -10,6 +10,11 @@ pub struct DbInventoryItem {
pub reorder_point: f64,
pub allow_fractional_units: bool,
pub display_unit: i64,
pub active: bool,
pub pims_id: Option<String>,
pub vetcove_id: Option<String>,
pub manufacturer_name: Option<String>,
pub manufacturer_id: Option<String>,
}
@ -23,7 +28,12 @@ pub async fn inventory_item_get_all(db: &SqlitePool, page_size: i64, page_num: i
name,
reorder_point,
allow_fractional_units,
display_unit
display_unit,
active,
pims_id,
vetcove_id,
manufacturer_name,
manufacturer_id
FROM
InventoryItem
LIMIT ? OFFSET ?
@ -49,7 +59,12 @@ pub async fn inventory_item_get_search(db: &SqlitePool,
name,
reorder_point,
allow_fractional_units,
display_unit
display_unit,
active,
pims_id,
vetcove_id,
manufacturer_name,
manufacturer_id
FROM
InventoryItem
WHERE InventoryItem.name LIKE ?
@ -71,7 +86,12 @@ pub async fn inventory_item_get_by_id(db: &SqlitePool, id: i64) -> Result<DbInve
name,
reorder_point,
allow_fractional_units,
display_unit
display_unit,
active,
pims_id,
vetcove_id,
manufacturer_name,
manufacturer_id
FROM
InventoryItem
WHERE InventoryItem.id = ?
@ -87,10 +107,10 @@ pub async fn add_inventory_item(db: &SqlitePool, name: &str, reorder_point: f64,
) -> Result<i64> {
let res = sqlx::query!(
r#"
INSERT INTO InventoryItem (name, reorder_point, allow_fractional_units, display_unit)
VALUES (?, ?, ?, (SELECT id from DisplayUnit WHERE abbreviation = ? ))
INSERT INTO InventoryItem (name, reorder_point, allow_fractional_units, display_unit, active)
VALUES (?, ?, ?, (SELECT id from DisplayUnit WHERE abbreviation = ? ), ?)
"#,
name, reorder_point, allow_fractional_units, display_unit_abbreviation
name, reorder_point, allow_fractional_units, display_unit_abbreviation, true
).execute(db).await?;
let new_id = res.last_insert_rowid();
@ -109,6 +129,7 @@ pub struct DbInventoryItemWithCount {
pub display_unit_str: String,
pub display_unit_abbreviation: String,
pub amount: Option<f64>,
pub active: bool,
}
pub async fn inventory_item_get_by_id_with_unit(db: &SqlitePool, id: i64) -> Result<DbInventoryItemWithCount> {
@ -123,7 +144,8 @@ pub async fn inventory_item_get_by_id_with_unit(db: &SqlitePool, id: i64) -> Res
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
(SELECT TOTAL(amount) as amt FROM Adjustment WHERE item = ?) as amount,
item.active as active
FROM
InventoryItem as item
JOIN DisplayUnit as display_unit ON item.display_unit = display_unit.id
@ -150,3 +172,20 @@ pub async fn does_inventory_item_allow_fractional_units(db: &SqlitePool, id: i64
Ok(res.allow_fractional_units)
}
pub async fn inventory_item_get_unit_abbreviation(db: &SqlitePool, id: i64) -> Result<String> {
let res = sqlx::query!(
r#"
SELECT
unit.abbreviation
FROM InventoryItem as item
JOIN DisplayUnit as unit
ON item.display_unit = unit.id
WHERE item.id = ?
"#,
id
)
.fetch_one(db).await?;
Ok(res.abbreviation)
}

@ -25,6 +25,7 @@ pub async fn init() -> anyhow::Result<SqlitePool> {
pub async fn connect_db(url: &str) -> anyhow::Result<SqlitePool> {
let options = SqliteConnectOptions::from_str(url)?
.foreign_keys(true)
.create_if_missing(true);
let exists = options.get_filename().exists();

@ -0,0 +1,6 @@
<div hx-get="/item/{{item_id}}/stats" hx-trigger="new-adjustment from:body" hx-swap="outerHTML" class="grid">
<article>Amount in stock: {{amount}} {{unit_abbreviation}}</article>
<article>Total Value: {{value}}</article>
</div>

@ -4,17 +4,37 @@
{% block content %}
<h3>{{item.name}}</h3>
<p>Reorder at: {{item.reorder_point}}</p>
<p>Amount in stock: {{item.amount}} {{item.display_unit_short}}</p>
<h2>{{item.name}} {% if !item.active %} <mark>Inactive</mark> {% endif %}</h2>
<section>
<div hx-get="/item/{{item_id}}/stats" hx-trigger="load" hx-swap="outerHTML">
</div>
<div hx-get="/item/{{item_id}}/adjustments" hx-trigger="load" hx-swap="outerHTML">
</div>
<div class="grid">
<article>Reorder at: {{item.reorder_point}}</article>
</div>
</section>
<div hx-get="/item/{{item_id}}/adjustment/sale" hx-trigger="load" hx-swap="outerHTML">
</div>
{% if item.active %}
<section>
<div class="grid">
<article>
<h3>Sale</h3>
<div hx-get="/item/{{item_id}}/adjustment/sale" hx-trigger="load" hx-swap="outerHTML">
</div>
</article>
<div hx-get="/item/{{item_id}}/adjustment/new-stock" hx-trigger="load" hx-swap="outerHTML">
<article>
<h3>New Stock</h3>
<div hx-get="/item/{{item_id}}/adjustment/new-stock" hx-trigger="load" hx-swap="outerHTML">
</div>
</article>
</div>
</section>
{% endif %}
<section>
<div hx-get="/item/{{item_id}}/adjustments" hx-trigger="load" hx-swap="outerHTML">
</div>
</section>
{% endblock %}
Loading…
Cancel
Save

Powered by TurnKey Linux.