Merge adjustment tables into one

demo-mode
Wes Holland 1 year ago
parent 74342dcc82
commit 5fd8e249b1

@ -2,6 +2,7 @@
CREATE TABLE IF NOT EXISTS User (
id INTEGER PRIMARY KEY NOT NULL,
email TEXT NOT NULL,
name TEXT NOT NULL,
role INTEGER NOT NULL,
tz_offset INTEGER NOT NULL,
@ -27,19 +28,7 @@ CREATE TABLE IF NOT EXISTS InventoryItem (
FOREIGN KEY(display_unit) REFERENCES DisplayUnit(id)
);
CREATE TABLE IF NOT EXISTS PositiveAdjustment (
id INTEGER PRIMARY KEY NOT NULL,
item INTEGER NOT NULL,
user INTEGER NOT NULL,
create_date DATETIME NOT NULL,
target_date DATETIME NOT NULL,
amount REAL NOT NULL,
unit_price INTEGER NOT NULL,
FOREIGN KEY(user) REFERENCES User(id),
FOREIGN KEY(item) REFERENCES InventoryItem(id)
);
CREATE TABLE IF NOT EXISTS NegativeAdjustment (
CREATE TABLE IF NOT EXISTS Adjustment (
id INTEGER PRIMARY KEY NOT NULL,
item INTEGER NOT NULL,
user INTEGER NOT NULL,
@ -47,22 +36,24 @@ CREATE TABLE IF NOT EXISTS NegativeAdjustment (
target_date INTEGER NOT NULL,
amount REAL NOT NULL,
reason INTEGER NOT NULL,
unit_price INTEGER,
FOREIGN KEY(user) REFERENCES User(id),
FOREIGN KEY(item) REFERENCES InventoryItem(id),
FOREIGN KEY(reason) REFERENCES NegativeAdjustmentReason(id)
FOREIGN KEY(reason) REFERENCES AdjustmentReason(id)
);
CREATE TABLE IF NOT EXISTS NegativeAdjustmentReason (
CREATE TABLE IF NOT EXISTS AdjustmentReason (
id INTEGER PRIMARY KEY NOT NULL,
name TEXT NOT NULL
);
INSERT INTO NegativeAdjustmentReason (id, name) VALUES
INSERT INTO AdjustmentReason (id, name) VALUES
(0,'unknown'),
(10,'sale'),
(20,'destruction'),
(25,'expiration'),
(30,'theft');
(30,'theft'),
(50,'new-stock');
CREATE TABLE IF NOT EXISTS DisplayUnit (
id INTEGER PRIMARY KEY NOT NULL,

@ -1,20 +1,21 @@
use crate::db::adjustment::{get_adjustments_target_date_range, DbAdjustment, DbAdjustmentWithUserAndItem};
use crate::error::{AppError, QueryExtractor};
use crate::session::SessionUser;
use crate::util::time::tz_offset_to_string;
use anyhow::Result;
use askama::Template;
use askama_axum::{IntoResponse, Response};
use axum::extract::State;
use axum_htmx::HxRequest;
use chrono::prelude::*;
use serde::Deserialize;
use sqlx::SqlitePool;
use tracing::info;
use chrono::prelude::*;
use crate::db::positive_adjustment::{get_positive_adjustments_target_date_range, DbPositiveAdjustment};
use crate::error::{AppError, QueryExtractor};
use crate::session::SessionUser;
use crate::util::time::tz_offset_to_string;
#[derive(Template)]
#[template(path = "history.html")]
struct HistoryLogTemplate {
items: Vec<DbPositiveAdjustment>,
items: Vec<HistoryDisplayItem>,
start_date: String,
start_time: String,
end_date: String,
@ -25,9 +26,94 @@ struct HistoryLogTemplate {
#[derive(Template)]
#[template(path = "history_item_fragment.html")]
struct HistoryLogItemFragmentTemplate {
items: Vec<DbPositiveAdjustment>
items: Vec<HistoryDisplayItem>
}
#[derive(Clone, Debug)]
struct HistoryDisplayItem {
pub create_date: String,
pub target_date: String,
pub user_name: String,
pub item_name: String,
pub item_unit: String,
pub item_unit_abbreviation: String,
pub item_type: HistoryItemEntry,
}
#[derive(Clone, Debug)]
struct PositiveAdjustmentDisplayItem {
pub amount: String,
pub unit_value: String,
}
pub fn int_cents_to_currency_string(i: i64) -> String {
let whole = i / 100;
let cents = i % 100;
format!("${}.{}", whole, cents)
}
impl From<DbAdjustment> for PositiveAdjustmentDisplayItem {
fn from(adjustment: DbAdjustment) -> Self {
Self {
amount: format!("{}", adjustment.amount),
unit_value: int_cents_to_currency_string(adjustment.unit_price.unwrap_or_default()),
}
}
}
#[derive(Clone, Debug)]
struct NegativeAdjustmentDisplayItem {
pub amount: String,
pub reason: String,
}
impl From<DbAdjustment> for NegativeAdjustmentDisplayItem {
fn from(adjustment: DbAdjustment) -> Self {
Self {
amount: format!("{}", adjustment.amount),
reason: adjustment.reason.into(),
}
}
}
#[derive(Clone, Debug)]
enum HistoryItemEntry {
PositiveAdjustment(PositiveAdjustmentDisplayItem),
NegativeAdjustment(NegativeAdjustmentDisplayItem),
}
impl HistoryDisplayItem {
pub fn from_adjustment(db_entry: DbAdjustmentWithUserAndItem, tz_offset: i32)
-> Result<Self> {
let simple_entry: DbAdjustment = db_entry.clone().into();
let timezone = FixedOffset::east_opt(tz_offset)
.ok_or(anyhow::anyhow!("Invalid timezone"))?;
let create_date = db_entry.create_date.with_timezone(&timezone)
.format("%Y-%m-%d %l:%M:%S %p").to_string();
let target_date = db_entry.target_date.with_timezone(&timezone)
.format("%Y-%m-%d %l:%M:%S %p").to_string();
let item_type = if simple_entry.amount > 0.0 {
HistoryItemEntry::PositiveAdjustment(simple_entry.into())
} else {
HistoryItemEntry::NegativeAdjustment(simple_entry.into())
};
Ok(Self {
create_date,
target_date,
user_name: db_entry.user_name,
item_name: db_entry.item_name,
item_unit: db_entry.item_unit,
item_unit_abbreviation: db_entry.item_unit_abbreviation,
item_type
})
}
}
/// Common query args for datetime ranges
#[derive(Debug, Deserialize)]
pub struct DatetimeRangeQueryArgs {
@ -50,12 +136,14 @@ pub async fn history_log_handler(
user: SessionUser
) -> Result<Response, AppError> {
info!("Query: {:?}", query);
let today = Local::now().naive_local().date();
let start_date = query.start_date.unwrap_or("2000-01-01".to_string());
let start_time = query.start_time.unwrap_or("00:00:00".to_string());
let end_date = query.end_date.unwrap_or(today.to_string());
let end_time = query.end_time.unwrap_or("11:59:59".to_string());
let end_time = query.end_time.unwrap_or("23:59:59".to_string());
let tz_offset = query.time_zone_offset.unwrap_or(user.tz_offset);
let timezone = FixedOffset::east_opt(tz_offset)
.ok_or(anyhow::anyhow!("Invalid timezone"))?;
@ -65,25 +153,38 @@ pub async fn history_log_handler(
let naive_end_date = end_date.parse::<NaiveDate>()?;
let naive_end_time = end_time.parse::<NaiveTime>()?;
let combined_start = naive_start_date
let local_start = naive_start_date
.and_time(naive_start_time)
.and_local_timezone(timezone)
.earliest()
.ok_or(anyhow::anyhow!("Invalid start"))?
.to_utc();
.ok_or(anyhow::anyhow!("Invalid start"))?;
let combined_end = naive_end_date
let combined_start = local_start.to_utc();
let local_end = naive_end_date
.and_time(naive_end_time)
.and_local_timezone(timezone)
.latest()
.ok_or(anyhow::anyhow!("Invalid start"))?
.to_utc();
.ok_or(anyhow::anyhow!("Invalid start"))?;
let combined_end = local_end.to_utc();
let start_date = local_start.naive_local().date().to_string();
let start_time = local_start.naive_local().time().to_string();
let end_date = local_end.naive_local().date().to_string();
let end_time = local_end.naive_local().time().to_string();
info!("Get items from: {} to {}", combined_start, combined_end);
let items = get_positive_adjustments_target_date_range(&db,
combined_start, combined_end).await?;
let items = DbAdjustmentWithUserAndItem::query_by_date_range(
&db, combined_start, combined_end, 1000, 0)
.await?
.into_iter()
.map(|x| {
HistoryDisplayItem::from_adjustment(x, tz_offset)
})
.collect::<Result<Vec<HistoryDisplayItem>>>()?;
info!("Item count: {}", items.len());

@ -135,7 +135,7 @@ pub async fn auth_authorized(
.context("failed to deserialize response as JSON")?;
// STEP 7 - Authorize the user at the application level
let db_user = match db::user::get_user_by_name(&db, &oauth_user_data.email).await? {
let db_user = match db::user::get_user_by_email(&db, &oauth_user_data.email).await? {
Some(user) => user,
None => {
return Ok(AppForbiddenResponse::new(&oauth_user_data.email, "application").into_response())

@ -0,0 +1,206 @@
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") }
}
}
}

@ -1,5 +1,5 @@
use std::str::FromStr;
use crate::db::user::{add_user, get_user_by_name, get_user_count, DbUserRole};
use crate::db::user::{add_user, get_user_by_email, get_user_count, DbUserRole};
use anyhow::{anyhow, Result};
use serde::Deserialize;
use sqlx::SqlitePool;
@ -14,6 +14,7 @@ struct BootstrapData {
#[derive(Debug, Deserialize)]
struct BootstrapUser {
email: String,
name: String,
role: String,
tz_offset: Option<String>,
@ -40,8 +41,8 @@ pub async fn bootstrap_database(db: &SqlitePool) -> Result<()> {
let tz = if let Some(tz) = &user.tz_offset {
i64::from_str(&tz).unwrap_or(DEFAULT_TIMEZONE)
} else { DEFAULT_TIMEZONE };
if get_user_by_name(db, &user.name).await?.is_none() {
let new_id = add_user(db, &user.name, role, tz).await?;
if get_user_by_email(db, &user.name).await?.is_none() {
let new_id = add_user(db, &user.email, &user.name, role, tz).await?;
info!("bootstrap new user {}:{} ({})", new_id, user.name, user.role);
}
}

@ -85,18 +85,15 @@ pub async fn inventory_item_get_by_id(db: &SqlitePool, id: i64) -> Result<DbInve
pub async fn sum_all_adjustments_for_item(db: &SqlitePool, id: i64) -> Result<f64> {
let res = sqlx::query!(
r#"
SELECT
(SELECT TOTAL(amount) FROM PositiveAdjustment WHERE item = ?) AS plus,
(SELECT TOTAL(amount) FROM NegativeAdjustment WHERE item = ?) AS minus
SELECT TOTAL(amount) as amt FROM Adjustment WHERE item = ?
"#,
id, id
id
)
.fetch_one(db).await?;
let plus: f64 = res.plus.unwrap_or_default();
let minus: f64 = res.minus.unwrap_or_default();
Ok(plus - minus)
let amt: f64 = res.amt.unwrap_or_default();
Ok(amt)
}
pub async fn add_inventory_item(db: &SqlitePool, name: &str, reorder_point: f64,

@ -1,8 +1,7 @@
pub mod inventory_item;
pub mod positive_adjustment;
pub mod adjustment;
mod bootstrap_data;
pub mod user;
pub mod negative_adjustment;
use crate::db::bootstrap_data::bootstrap_database;
use anyhow::Context;

@ -1,115 +0,0 @@
use serde::Serialize;
use sqlx::SqlitePool;
use anyhow::Result;
use chrono::{DateTime, Utc};
use tracing::error;
#[derive(Debug, Serialize)]
#[derive(sqlx::FromRow)]
pub struct DbNegativeAdjustment {
pub id: i64,
pub item: i64,
pub user: i64,
pub create_date: i64,
pub target_date: i64,
pub amount: f64,
pub reason: DbNegativeAdjustmentReason,
}
pub async fn add_negative_adjustment(db: &SqlitePool, item: i64, user: i64,
create_date: DateTime<Utc>, target_date: DateTime<Utc>,
amount: f64, reason: DbNegativeAdjustmentReason) -> Result<i64> {
let reason: i64 = reason.into();
let res = sqlx::query!(
r#"
INSERT INTO NegativeAdjustment (item, user, create_date, target_date, amount, reason)
VALUES (?, ?, ?, ?, ?, ?)
"#,
item, user, create_date, target_date, amount, reason
).execute(db).await?;
let new_id = res.last_insert_rowid();
Ok(new_id)
}
pub async fn get_negative_adjustments_target_date_range(
db: &SqlitePool, start_date: DateTime<Utc>, end_date: DateTime<Utc>
) -> Result<Vec<DbNegativeAdjustment>> {
sqlx::query_as!(
DbNegativeAdjustment,
r#"
SELECT id, item, user, create_date, target_date, amount, reason
FROM NegativeAdjustment
WHERE target_date >= ? AND target_date <= ?
"#,
start_date, end_date
)
.fetch_all(db)
.await.map_err(Into::into)
}
#[derive(Debug, Clone, Copy, Serialize)]
pub enum DbNegativeAdjustmentReason {
Unknown,
Sale,
Destruction,
Expiration,
Theft,
}
impl Into<i64> for DbNegativeAdjustmentReason {
fn into(self) -> i64 {
match self {
Self::Unknown => 0,
Self::Sale => 10,
Self::Destruction => 20,
Self::Expiration => 25,
Self::Theft => 30,
}
}
}
impl From<i64> for DbNegativeAdjustmentReason {
fn from(item: i64) -> Self {
match item {
0 => Self::Unknown,
10 => Self::Sale,
20 => Self::Destruction,
25 => Self::Expiration,
30 => Self::Theft,
_ => {
error!("unknown negative adjustment reason value: {}", item);
Self::Unknown
}
}
}
}
impl TryFrom<&str> for DbNegativeAdjustmentReason {
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),
_ => Err(anyhow::anyhow!("unknown negative adjustment reason"))
}
}
}
impl Into<String> for DbNegativeAdjustmentReason {
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") }
}
}
}

@ -1,53 +0,0 @@
use serde::Serialize;
use anyhow::Result;
use chrono::{DateTime, Utc};
use sqlx::SqlitePool;
#[derive(Debug, Serialize)]
#[derive(sqlx::FromRow)]
pub struct DbPositiveAdjustment {
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: i64,
}
pub async fn add_positive_adjustment(db: &SqlitePool, item: i64, user: i64,
create_date: DateTime<Utc>, target_date: DateTime<Utc>,
amount: f64, unit_price: i64)
-> Result<i64> {
let res = sqlx::query!(
r#"
INSERT INTO PositiveAdjustment (item, user, create_date, target_date, amount, unit_price)
VALUES (?, ?, ?, ?, ?, ?)
"#,
item, user, create_date, target_date, amount, unit_price
).execute(db).await?;
let new_id = res.last_insert_rowid();
Ok(new_id)
}
pub async fn get_positive_adjustments_target_date_range(
db: &SqlitePool, start_date: DateTime<Utc>, end_date: DateTime<Utc>
) -> Result<Vec<DbPositiveAdjustment>> {
sqlx::query_as::<_, DbPositiveAdjustment>(r#"
SELECT id, item, user,
create_date,
target_date,
amount, unit_price
FROM PositiveAdjustment
WHERE target_date >= ? AND target_date <= ?
"#,)
.bind(start_date)
.bind(end_date)
.fetch_all(db)
.await.map_err(Into::into)
}

@ -6,36 +6,39 @@ use sqlx::SqlitePool;
#[derive(sqlx::FromRow)]
pub struct DbUser {
pub id: i64,
pub email: String,
pub name: String,
pub role: i64,
pub tz_offset: i64,
}
pub async fn get_user_by_name(db: &SqlitePool, user: &str) -> Result<Option<DbUser>> {
pub async fn get_user_by_email(db: &SqlitePool, email: &str) -> Result<Option<DbUser>> {
sqlx::query_as::<_, DbUser>(
r#"
SELECT
id,
name,
email,
name,
role,
tz_offset
FROM
User
WHERE
name = ?
email = ?
"#)
.bind(user)
.bind(email)
.fetch_optional(db).await
.map_err(From::from)
}
pub async fn add_user(db: &SqlitePool, name: &str, role: DbUserRole, tz_offset: i64) -> Result<i64> {
pub async fn add_user(db: &SqlitePool, email: &str, name: &str, role: DbUserRole, tz_offset: i64) -> Result<i64> {
let role = role.value();
let res = sqlx::query(
r#"
INSERT INTO User(name, role, tz_offset)
VALUES (?, ?, ?)
INSERT INTO User(email, name, role, tz_offset)
VALUES (?, ?, ?, ?)
"#)
.bind(email)
.bind(name)
.bind(role)
.bind(tz_offset)

@ -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::positive_adjustment::add_positive_adjustment;
use crate::db::adjustment::add_new_stock_adjustment;
#[derive(Debug, serde::Deserialize)]
struct CatalogRecord {
@ -38,8 +38,8 @@ 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_positive_adjustment(&db, new_entry_id,
user_id, timestamp, timestamp, record.quantity, record.unit_price).await?;
let new_positive_adjustment = add_new_stock_adjustment(&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);
}

@ -10,11 +10,11 @@
<div class="grid">
<fieldset>
<label for="start-date">Start Date</label>
<input type="date" id="start-date" name="start" value="{{ start_date }}" />
<input type="date" id="start-date" name="start-date" value="{{ start_date }}" />
</fieldset>
<fieldset>
<label for="start-time">Start Time</label>
<input type="time" id="start-time" name="start" value="{{ start_time }}"/>
<input type="time" id="start-time" name="start-time" value="{{ start_time }}"/>
<small>Timezone {{ time_zone }}</small>
</fieldset>
</div>
@ -22,11 +22,11 @@
<div class="grid">
<fieldset>
<label for="end-date">End Date</label>
<input type="date" id="end-date" name="end" value="{{ end_date }}"/>
<input type="date" id="end-date" name="end-date" value="{{ end_date }}"/>
</fieldset>
<fieldset>
<label for="end-time">End Time</label>
<input type="time" id="end-time" name="end" value="{{ end_time }}"/>
<input type="time" id="end-time" name="end-time" value="{{ end_time }}"/>
<small>Timezone {{ time_zone }}</small>
</fieldset>
</div>

@ -1,9 +1,18 @@
{% for item in items %}
<article id="item-{{item.id}}-card">
<article>
<div class="grid">
<p>{{ item.item }}</p>
<p>{{ item.amount }}</p>
<p>{{ item.create_date }}</p>
<p>{{ item.item_name }}</p>
<p>{{ item.user_name }}</p>
<p>
{% match item.item_type %}
{% when HistoryItemEntry::PositiveAdjustment with (entry) %}
{{ entry.amount }} @ {{ entry.unit_value }}
{% when HistoryItemEntry::NegativeAdjustment with (entry) %}
{{ entry.amount }} {{ entry.reason }}
{% endmatch %}
</p>
</div>
</article>
{% endfor %}
Loading…
Cancel
Save

Powered by TurnKey Linux.