Compare commits
No commits in common. "feature/better-report" and "main" have entirely different histories.
feature/be
...
main
@ -133,11 +133,8 @@ type alias Context =
|
|||||||
|
|
||||||
type alias Globals =
|
type alias Globals =
|
||||||
{ locations : List Location
|
{ locations : List Location
|
||||||
, defaultLocation : Location
|
|
||||||
, groups : List Group
|
, groups : List Group
|
||||||
, defaultGroup : Group
|
|
||||||
, taxGroups : List TaxGroup
|
, taxGroups : List TaxGroup
|
||||||
, defaultTaxGroup : TaxGroup
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type State
|
type State
|
||||||
@ -161,7 +158,7 @@ type Msg
|
|||||||
= SetSearchTerm String
|
= SetSearchTerm String
|
||||||
| SubmitSearch
|
| SubmitSearch
|
||||||
| ReceiveSearchResults (Result Http.Error (List SearchResult))
|
| ReceiveSearchResults (Result Http.Error (List SearchResult))
|
||||||
| GotoItemEditor IEInit
|
| GotoItemEditor SearchResult
|
||||||
| SetBarcode String
|
| SetBarcode String
|
||||||
| SetName String
|
| SetName String
|
||||||
| SetSalesUnits String
|
| SetSalesUnits String
|
||||||
@ -172,10 +169,6 @@ type Msg
|
|||||||
| SetLocation String
|
| SetLocation String
|
||||||
| SetTaxGroup String
|
| SetTaxGroup String
|
||||||
|
|
||||||
type IEInit
|
|
||||||
= IEInitBarcode String
|
|
||||||
| IEInitSearchResult SearchResult
|
|
||||||
|
|
||||||
-- Update logic: State machine etc.
|
-- Update logic: State machine etc.
|
||||||
|
|
||||||
update msg { globals, state } =
|
update msg { globals, state } =
|
||||||
@ -197,21 +190,7 @@ updateState msg globals state = case state of
|
|||||||
)
|
)
|
||||||
ReceiveSearchResults (Ok searchResults) ->
|
ReceiveSearchResults (Ok searchResults) ->
|
||||||
(ItemSearch { model | searchResults = searchResults }, Cmd.none)
|
(ItemSearch { model | searchResults = searchResults }, Cmd.none)
|
||||||
GotoItemEditor (IEInitBarcode barcode) ->
|
GotoItemEditor searchResult ->
|
||||||
( ItemEditor
|
|
||||||
{ barcode = barcode
|
|
||||||
, name = ""
|
|
||||||
, calculator = Calculator.init 0
|
|
||||||
, netUnitPrice = NumberInput.fromFloat 0
|
|
||||||
, grossUnitPrice = NumberInput.fromFloat 0
|
|
||||||
, salesUnits = NumberInput.fromInt 0
|
|
||||||
, group = Select.init (.id >> String.fromInt) (.name) globals.defaultGroup globals.groups
|
|
||||||
, location = Select.init (.id >> String.fromInt) (.name) globals.defaultLocation globals.locations
|
|
||||||
, taxGroup = Select.init (.id >> String.fromInt) (.description) globals.defaultTaxGroup globals.taxGroups
|
|
||||||
}
|
|
||||||
, Cmd.none
|
|
||||||
)
|
|
||||||
GotoItemEditor (IEInitSearchResult searchResult) ->
|
|
||||||
case find (\tg -> tg.id == searchResult.taxGroupId) globals.taxGroups of
|
case find (\tg -> tg.id == searchResult.taxGroupId) globals.taxGroups of
|
||||||
Nothing -> (state, Cmd.none)
|
Nothing -> (state, Cmd.none)
|
||||||
Just taxGroup ->
|
Just taxGroup ->
|
||||||
@ -266,21 +245,14 @@ suggestedGrossPrice netPrice percentage =
|
|||||||
|
|
||||||
view { globals, state } = case state of
|
view { globals, state } = case state of
|
||||||
ItemSearch model ->
|
ItemSearch model ->
|
||||||
div []
|
fieldset []
|
||||||
[ div []
|
[ legend [] [ text "Vorlage für Auftrag wählen" ]
|
||||||
[ if model.searchTerm == ""
|
, Html.form [ onSubmit SubmitSearch ]
|
||||||
then button [ disabled True ] [ text "Neuer Artikel" ]
|
[ div [ class "form-input" ]
|
||||||
else button [ onClick <| GotoItemEditor <| IEInitBarcode model.searchTerm ] [ text <| "Neuer Artikel mit Barcode " ++ model.searchTerm ]
|
[ label [ for "search-term", title "Barcode oder Name" ] [ text "Suchbegriff" ]
|
||||||
]
|
, input [ onInput SetSearchTerm, value model.searchTerm, id "search-term" ] []
|
||||||
, fieldset []
|
|
||||||
[ legend [] [ text "Vorlage für Auftrag wählen" ]
|
|
||||||
, Html.form [ onSubmit SubmitSearch ]
|
|
||||||
[ div [ class "form-input" ]
|
|
||||||
[ label [ for "search-term", title "Barcode oder Name" ] [ text "Suchbegriff" ]
|
|
||||||
, input [ onInput SetSearchTerm, value model.searchTerm, id "search-term" ] []
|
|
||||||
]
|
|
||||||
, table [] <| searchResultHeaders :: List.map viewSearchResult model.searchResults
|
|
||||||
]
|
]
|
||||||
|
, table [] <| searchResultHeaders :: List.map viewSearchResult model.searchResults
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
ItemEditor model ->
|
ItemEditor model ->
|
||||||
@ -413,7 +385,7 @@ viewSearchResult model =
|
|||||||
, td [] [ text model.locationName ]
|
, td [] [ text model.locationName ]
|
||||||
, td [] [ text <| showBool model.available ]
|
, td [] [ text <| showBool model.available ]
|
||||||
, td []
|
, td []
|
||||||
[ Html.form [ onSubmit <| GotoItemEditor <| IEInitSearchResult model ]
|
[ Html.form [ onSubmit <| GotoItemEditor model ]
|
||||||
[ button [] [ text "Als Vorlage verwenden" ]
|
[ button [] [ text "Als Vorlage verwenden" ]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
@ -1,43 +1,49 @@
|
|||||||
-- parameters:
|
|
||||||
--
|
|
||||||
-- location_id: Location to generate the report for
|
|
||||||
|
|
||||||
WITH
|
WITH
|
||||||
sales_by_item_id AS (
|
most_recent_sales AS (
|
||||||
SELECT
|
SELECT DISTINCT ON (inventory_line)
|
||||||
inventory_line AS item_id,
|
inventory_line, snack_sales_log_id, snack_sales_log_timestamp AS most_recent_sale
|
||||||
max(snack_sales_log_timestamp) AS last_sold,
|
|
||||||
count(*)::int AS units_sold
|
|
||||||
FROM garfield.snack_sales_log
|
FROM garfield.snack_sales_log
|
||||||
WHERE type_id = 'SNACK_BUY'
|
ORDER BY inventory_line ASC, snack_sales_log_timestamp DESC
|
||||||
AND inventory_line IS NOT NULL
|
|
||||||
AND snack_sales_log_timestamp > NOW() - INTERVAL '120 days'
|
|
||||||
AND location_id = %(location_id)s
|
|
||||||
GROUP BY item_id
|
|
||||||
),
|
),
|
||||||
sales_by_barcode AS (
|
enhanced_overview1 AS (
|
||||||
SELECT
|
SELECT
|
||||||
item_barcode,
|
inventory_items.item_id,
|
||||||
max(name) AS name,
|
inventory_items.item_barcode,
|
||||||
sum(units_sold) AS units_sold,
|
inventory_items.name,
|
||||||
round(avg((units_sold / extract(epoch FROM (CASE WHEN available THEN now() ELSE last_sold END) - bought)) * 86400)::numeric, 2) AS sold_per_day
|
units_left,
|
||||||
FROM sales_by_item_id
|
inventory_items.sales_units,
|
||||||
|
correction_delta,
|
||||||
|
location_name,
|
||||||
|
location,
|
||||||
|
CASE
|
||||||
|
WHEN snack_sales_log_id IS NULL THEN 0
|
||||||
|
ELSE sales / (EXTRACT(EPOCH FROM (
|
||||||
|
CASE
|
||||||
|
WHEN units_left <= 0 THEN most_recent_sale
|
||||||
|
ELSE NOW()
|
||||||
|
END
|
||||||
|
)) - EXTRACT(EPOCH FROM bought)) * 24 * 3600
|
||||||
|
END AS per_day
|
||||||
|
FROM garfield.inventory_item_overview
|
||||||
LEFT JOIN garfield.inventory_items USING (item_id)
|
LEFT JOIN garfield.inventory_items USING (item_id)
|
||||||
GROUP BY item_barcode
|
LEFT JOIN most_recent_sales ON item_id = inventory_line
|
||||||
),
|
),
|
||||||
current_inventory AS (
|
enhanced_overview2 AS (
|
||||||
SELECT
|
SELECT
|
||||||
item_barcode,
|
*,
|
||||||
sum(units_left)::int AS units_left
|
CASE
|
||||||
FROM all_inventory_item_overview
|
WHEN per_day = 0 THEN NULL
|
||||||
WHERE available
|
ELSE GREATEST(0, units_left / per_day)
|
||||||
AND location = %(location_id)s
|
END AS days_left
|
||||||
GROUP BY item_barcode
|
FROM enhanced_overview1
|
||||||
)
|
)
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
sales_by_barcode.*,
|
*,
|
||||||
COALESCE(current_inventory.units_left, 0)::int AS units_left
|
CASE
|
||||||
FROM sales_by_barcode
|
WHEN days_left IS NULL THEN NULL
|
||||||
LEFT JOIN current_inventory USING (item_barcode)
|
ELSE GREATEST(0, (60 - days_left) * per_day)
|
||||||
ORDER BY units_sold DESC
|
END AS for_two_months
|
||||||
|
FROM enhanced_overview2
|
||||||
|
WHERE (%(location_id)s IS NULL OR location = %(location_id)s)
|
||||||
|
ORDER BY days_left ASC, per_day DESC
|
||||||
|
@ -94,13 +94,10 @@ def new_order():
|
|||||||
with db.run_query("entry/get_tax_groups.sql") as cursor:
|
with db.run_query("entry/get_tax_groups.sql") as cursor:
|
||||||
tax_groups = cursor.fetchall()
|
tax_groups = cursor.fetchall()
|
||||||
|
|
||||||
selected_location = current_user.data.location
|
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"entry/new-order.html",
|
"entry/new-order.html",
|
||||||
groups=groups,
|
groups=groups,
|
||||||
locations=locations,
|
locations=locations,
|
||||||
default_location=next(location for location in locations if location["id"] == selected_location["location_id"]) if selected_location is not None else locations[0],
|
|
||||||
tax_groups=tax_groups
|
tax_groups=tax_groups
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,17 +22,11 @@ def index():
|
|||||||
@bp.get("/report")
|
@bp.get("/report")
|
||||||
def read_report():
|
def read_report():
|
||||||
location = current_user.data.location
|
location = current_user.data.location
|
||||||
|
|
||||||
if location is None:
|
|
||||||
# TODO: Error handling
|
|
||||||
return "please select a location in order to generate a report", 400
|
|
||||||
|
|
||||||
items = db.run_query("get_inventory_report.sql", {
|
items = db.run_query("get_inventory_report.sql", {
|
||||||
"location_id": location["location_id"]
|
"location_id": None if location is None else location["location_id"]
|
||||||
}).fetchall()
|
}).fetchall()
|
||||||
|
|
||||||
return render_template("inventory/read_report.html", **{
|
return render_template("inventory/read_report.html", **{
|
||||||
"location": location,
|
|
||||||
"items": items
|
"items": items
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -40,9 +40,6 @@ nav > ul > li + li:before {
|
|||||||
.--centered {
|
.--centered {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
.--not-important {
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
@keyframes wiggle {
|
@keyframes wiggle {
|
||||||
0%, 100% { margin-top: 0; }
|
0%, 100% { margin-top: 0; }
|
||||||
50% { margin-top: -0.5em; }
|
50% { margin-top: -0.5em; }
|
||||||
@ -58,10 +55,6 @@ th {
|
|||||||
body {
|
body {
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
}
|
}
|
||||||
/* hide the menu when printing */
|
|
||||||
header {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.form-input > label {
|
.form-input > label {
|
||||||
font-size: .8em;
|
font-size: .8em;
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li{{ " class=current-page" if request.path == "/" else "" }}><a href="/">Home</a></li>
|
<li{{ " class=current-page" if request.path == "/" else "" }}><a href="/">Home</a></li>
|
||||||
<li{{ " class=current-page" if request.path.startswith("/inventory") else "" }}><a href="/inventory">Inventar</a></li>
|
<li{{ " class=current-page" if request.path.startswith("/inventory") else "" }}><a href="/inventory">Inventar</a></li>
|
||||||
<li><a href="/inventory/report">Einkäuferbericht</a></li>
|
|
||||||
<li{{ " class=current-page" if request.path.startswith("/entry") else "" }}><a href="/entry">Eintragen</a></li>
|
<li{{ " class=current-page" if request.path.startswith("/entry") else "" }}><a href="/entry">Eintragen</a></li>
|
||||||
<li{{ " class=current-page" if request.path.startswith("/location") else "" }}>
|
<li{{ " class=current-page" if request.path.startswith("/location") else "" }}>
|
||||||
<a href="/location">
|
<a href="/location">
|
||||||
|
@ -8,11 +8,8 @@ Elm.Entry.init({
|
|||||||
node: document.querySelector('.entry-app'),
|
node: document.querySelector('.entry-app'),
|
||||||
flags: {
|
flags: {
|
||||||
locations: {{ to_json(locations) | safe }},
|
locations: {{ to_json(locations) | safe }},
|
||||||
defaultLocation: {{ to_json(default_location) | safe }},
|
|
||||||
groups: {{ to_json(groups) | safe }},
|
groups: {{ to_json(groups) | safe }},
|
||||||
defaultGroup: {{ to_json(groups[0]) | safe }},
|
taxGroups: {{ to_json(tax_groups) | safe }}
|
||||||
taxGroups: {{ to_json(tax_groups) | safe }},
|
|
||||||
defaultTaxGroup: {{ to_json(tax_groups[0]) | safe }},
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,29 +1,29 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Einkäuferbericht für {{ location.location_name }}</h2>
|
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
<th>Barcode</th>
|
<th>Barcode</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Inventar</th>
|
<th>Inventar</th>
|
||||||
<th title="In den letzten 4 Monaten verkauft">Verkauft</th>
|
<th>Gesamt</th>
|
||||||
|
<th>Raum</th>
|
||||||
<th>Verbrauch [1/d]</th>
|
<th>Verbrauch [1/d]</th>
|
||||||
<th>Verbrauch [1/60d]</th>
|
|
||||||
<!--
|
|
||||||
<th title="Estimated Time Until Empty">ETUE [d]</th>
|
<th title="Estimated Time Until Empty">ETUE [d]</th>
|
||||||
<th>Für 2m</th>
|
<th>Für 2m</th>
|
||||||
-->
|
|
||||||
</tr>
|
</tr>
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<tr{% if item.units_left >= item.units_sold %} class="--not-important"{% endif %}>
|
<tr>
|
||||||
|
<td><a href="/inventory/item/{{ item.item_id }}">{{ item.item_id }}</a></td>
|
||||||
<td><code>{{ item.item_barcode }}</code></td>
|
<td><code>{{ item.item_barcode }}</code></td>
|
||||||
<td>{{ item.name }}</td>
|
<td>{{ item.name }}</td>
|
||||||
<td class="--align-right">{{ item.units_left }}</td>
|
<td class="--align-right">{{ item.units_left }}</td>
|
||||||
<td class="--align-right">{{ item.units_sold }}</td>
|
<td class="--align-right">{{ item.sales_units + item.correction_delta }}</td>
|
||||||
<td class="--align-right">{{ item.sold_per_day }}</td>
|
<td>{{ item.location_name }}</td>
|
||||||
<td class="--align-right">{{ item.sold_per_day * 60 }}</td>
|
<td class="--align-right">{{ item.per_day|round(2) }}</td>
|
||||||
|
<td class="--align-right">{% if item.days_left != None %}{{ item.days_left|round(1) }}{% endif %}</td>
|
||||||
|
<td class="--align-right">{% if item.for_two_months %}{{ item.for_two_months|round(1) }}{% endif %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user