summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md6
-rw-r--r--data/templates/css/base.css4
-rw-r--r--data/templates/menu.html18
-rw-r--r--data/templates/products/entry.html41
-rw-r--r--data/templates/products/index.html11
-rw-r--r--data/templates/products/inventory.html49
-rw-r--r--docker/Dockerfile1
-rwxr-xr-xdocker/init.sh2
-rw-r--r--src/database/database.vala11
-rw-r--r--src/database/db-interface.vala3
-rw-r--r--src/meson.build2
-rw-r--r--src/pdf-stock/pdf-stock-interface.vala2
-rw-r--r--src/pdf-stock/pdf-stock.vala5
-rw-r--r--src/pdf-stock/test.vala2
-rw-r--r--src/web/main.vala2
-rw-r--r--src/web/web.vala141
16 files changed, 283 insertions, 17 deletions
diff --git a/README.md b/README.md
index edcc8f0..aa6d2b6 100644
--- a/README.md
+++ b/README.md
@@ -79,3 +79,9 @@ You can control display power via MQTT by configuring the MQTT settings (i.e. BR
Edit the Logo in the logo.txt File.
A helpful tool you will found here [http://patorjk.com/software/taag/](http://patorjk.com/software/taag/)
+
+=== Some Vala resources ===
+
+* https://wiki.gnome.org/Projects/Vala/ValaForJavaProgrammers
+* https://valadoc.org/
+* https://getbootstrap.com/2.3.2/ \ No newline at end of file
diff --git a/data/templates/css/base.css b/data/templates/css/base.css
index e273808..2444eca 100644
--- a/data/templates/css/base.css
+++ b/data/templates/css/base.css
@@ -84,6 +84,10 @@ table.user-entry td {
min-width: 350px;
}
+h1 {
+ margin-bottom: 30px;
+}
+
@media print {
.navbar-fixed-top {
display: none !important;
diff --git a/data/templates/menu.html b/data/templates/menu.html
index 18e30a0..a1a0930 100644
--- a/data/templates/menu.html
+++ b/data/templates/menu.html
@@ -2,7 +2,23 @@
<ul class="navbar-nav nav">
<li class="{{{MENU.home}}}"><a href="/">Home</a></li>
- <li class="{{{MENU.products}}}"><a href="/products">Products</a></li>
+ <li class="{{{MENU.products}}} dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">Products
+ <b class="caret"></b>
+ </a>
+ <ul class="dropdown-menu">
+ <li>
+ <a href="/products">List</a>
+ </li>
+ <li>
+ <a href="/products/bestbefore">Best before dates</a>
+ </li>
+ <li>
+ <a href="/products/inventory">Start inventory</a>
+ </li>
+ </ul>
+ </li>
+
<li class="{{{MENU.aliases}}}"><a href="/aliases">Aliases</a></li>
<li class="{{{MENU.cashbox}}} {{{AUTH_CASHBOX}}}"><a href="/cashbox">Cashbox</a></li>
<!--
diff --git a/data/templates/products/entry.html b/data/templates/products/entry.html
index ec13aa6..64b2aa4 100644
--- a/data/templates/products/entry.html
+++ b/data/templates/products/entry.html
@@ -30,14 +30,39 @@
</table>
<div id="restock" style="display: {{{ISADMIN}}};">
- <form action="/products/{{{EAN}}}/restock" class="form-inline">
- <input class="form-control" name="amount" type="number" min="1" placeholder="Amount" title="Amount" />
- <input class="form-control" name="price" type="number" step="0.01" min="0.00" placeholder="Price" title="Price per Piece" />
- <select class="form-control" name="supplier" title="Supplier">
- {{{SUPPLIERS}}}
- </select>
- <input class="form-control" name="best_before_date" type="date" title="Best Before Date (YYYY-MM-DD)" />
- <button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span></button>
+
+ <form action="/products/{{{EAN}}}/restock" class="form-horizontal">
+ <div class="control-group">
+ <label class="control-label">Amount</label>
+ <div class="controls">
+ <input name="amount" type="number" min="1" placeholder="Amount" />
+ </div>
+ </div>
+ <div class="control-group">
+ <label class="control-label">Price per Piece</label>
+ <div class="controls">
+ <input name="price" type="number" step="0.01" min="0.00" placeholder="Price" />
+ </div>
+ </div>
+ <div class="control-group">
+ <label class="control-label">Suppplier</label>
+ <div class="controls">
+ <select name="supplier">
+ {{{SUPPLIERS}}}
+ </select>
+ </div>
+ </div>
+ <div class="control-group">
+ <label class="control-label">Best Before Date</label>
+ <div class="controls">
+ <input name="best_before_date" type="date" />
+ </div>
+ </div>
+ <div class="control-group">
+ <div class="controls">
+ <button type="submit" class="btn btn-primary"><i class="icon-plus"></i> Restock</button>
+ </div>
+ </div>
</form>
</div>
diff --git a/data/templates/products/index.html b/data/templates/products/index.html
index 8ece21a..ba7e08c 100644
--- a/data/templates/products/index.html
+++ b/data/templates/products/index.html
@@ -1,8 +1,13 @@
-<a href="/products/bestbefore">product list with best before dates</a>
-
<table id="producttable" class="table table-bordered table-striped table-condensed">
<thead>
- <tr><th>EAN</th></th><th>Name</th><th>Category</th><th>Amount</th><th>Memberprice</th><th>Guestprice</th></tr>
+ <tr>
+ <th>EAN</th>
+ <th>Name</th>
+ <th>Category</th>
+ <th>Amount</th>
+ <th>Memberprice</th>
+ <th>Guestprice</th>
+ </tr>
</thead>
<tbody>
{{{DATA}}}
diff --git a/data/templates/products/inventory.html b/data/templates/products/inventory.html
new file mode 100644
index 0000000..eb02081
--- /dev/null
+++ b/data/templates/products/inventory.html
@@ -0,0 +1,49 @@
+<h1>Inventory <small>Thanks that you do it! </small></h1>
+
+<div class="{{{SUCESS_HIDDEN}}} alert alert-success">
+ <strong>Success!</strong> Every change was added. Thank you!
+</div>
+
+<div class="alert alert-info">
+ <strong>Tip!</strong> You can download an inventory PDF to collect the data offline.
+ <a href="/products/inventory-pdf" target="_blank" class="btn btn-primary">Only products with amount > 0</a>
+ <a href="/products/inventory-pdf?all=1" target="_blank" class="btn btn-primary">All products</a>
+</div>
+
+<form action="/products/inventory" method="POST" class="form-horizontal">
+ <div class="{{{RESTOCK_HIDDEN}}} form-group">
+ <label class="control-label col-sm-3">Select the supplier for the restock action.</label>
+ <div class="col-sm-3">
+ <select name="supplierId" class="form-control">
+ {{{SUPPLIERS}}}
+ </select>
+ </div>
+ </div>
+ <div class="{{{RESTOCK_HIDDEN}}} form-group">
+ <label class="control-label col-sm-3">Select the Shop User for loss transaction.</label>
+ <div class="col-sm-3">
+ <select name="userId" class="form-control">
+ {{{USERS}}}
+ </select>
+ </div>
+ </div>
+ <table class="sortable table table-bordered table-striped table-condensed">
+ <thead>
+ <tr>
+ <th>EAN</th>
+ <th>Name</th>
+ <th>Category</th>
+ <th>Expected Amount</th>
+ <th>Real Amount</th>
+ </tr>
+ </thead>
+ <tbody>
+ {{{DATA}}}
+ </tbody>
+ </table>
+
+ <div class="pull-right">
+ {{{ACTION}}}
+ </div>
+
+</form> \ No newline at end of file
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 53a9048..c73cf1e 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -15,6 +15,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libgmime-2.6-dev \
libarchive-dev \
libgstreamer1.0-dev \
+ rygel-playbin \
librsvg2-dev \
libsqlite3-dev \
libpango1.0-dev \
diff --git a/docker/init.sh b/docker/init.sh
index 818fc45..bcd6f6b 100755
--- a/docker/init.sh
+++ b/docker/init.sh
@@ -1,5 +1,7 @@
#!/bin/bash
+export LD_LIBRARY_PATH=/mnt/serial-barcode-scanner/libcairobarcode
+
dbus-daemon --system
dpkg-buildpackage -b -nc && \
apt install -y --no-install-recommends ./../shopsystem_*_amd64.deb && \
diff --git a/src/database/database.vala b/src/database/database.vala
index 8d1bb84..52c5dd2 100644
--- a/src/database/database.vala
+++ b/src/database/database.vala
@@ -122,6 +122,7 @@ public class DataBase : Object {
queries["total_sales"] = "SELECT SUM(price) FROM invoice WHERE user >= 0 AND timestamp >= ?";
queries["total_profit"] = "SELECT SUM(price - (SELECT price FROM purchaseprices WHERE product = productid)) FROM invoice WHERE user >= 0 AND timestamp >= ?";
queries["user_get_ids"] = "SELECT id FROM users WHERE id > 0";
+ queries["system_user_get_ids"] = "SELECT id FROM users WHERE id <= 0";
queries["user_replace"] = "INSERT OR REPLACE INTO users ('id', 'email', 'firstname', 'lastname', 'gender', 'street', 'plz', 'city', 'pgp', 'hidden', 'disabled', 'joined_at', 'sound_theme') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, (select sound_theme from users where id = ?))";
queries["user_auth_create"] = "INSERT OR IGNORE INTO authentication (user) VALUES (?)";
queries["user_disable"] = "UPDATE users SET disabled = ? WHERE id = ?";
@@ -874,6 +875,16 @@ public class DataBase : Object {
return result;
}
+ public int[] get_system_member_ids() throws DBusError, IOError, DatabaseError {
+ int[] result = {};
+
+ statements["system_user_get_ids"].reset();
+ while(statements["system_user_get_ids"].step() == Sqlite.ROW)
+ result += statements["system_user_get_ids"].column_int(0);
+
+ return result;
+ }
+
public void user_disable(int user, bool value) throws DBusError, IOError, DatabaseError {
int rc;
diff --git a/src/database/db-interface.vala b/src/database/db-interface.vala
index 63a0148..f2d3e87 100644
--- a/src/database/db-interface.vala
+++ b/src/database/db-interface.vala
@@ -45,7 +45,8 @@ public interface Database : Object {
public abstract int64 get_first_purchase(int user) throws DBusError, IOError;
public abstract int64 get_last_purchase(int user) throws DBusError, IOError;
public abstract StatsInfo get_stats_info() throws DBusError, IOError;
- public abstract int[] get_member_ids() throws DBusError, IOError;
+ public abstract int[] get_member_ids() throws DBusError, IOError, DatabaseError;
+ public abstract int[] get_system_member_ids() throws DBusError, IOError, DatabaseError;
public abstract void user_disable(int user, bool value) throws DBusError, IOError, DatabaseError;
public abstract void user_replace(UserInfo u) throws DBusError, IOError, DatabaseError;
public abstract bool user_is_disabled(int user) throws DBusError, IOError, DatabaseError;
diff --git a/src/meson.build b/src/meson.build
index 49b57fa..8f7ed3d 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -49,4 +49,4 @@ executable('shop-test-pdf-stock', ['pdf-stock/test.vala', 'pdf-stock/pdf-stock-i
executable('shop-pgp', ['pgp/main.vala', 'pgp/pgp.vala', 'pgp/pgp-interface.vala', 'config/config-interface.vala'], dependencies: [gio_dep, gpgme_dep, archive_dep], c_args: ['-D_FILE_OFFSET_BITS=64'], install: true, install_dir: join_paths(get_option('libexecdir'), 'shopsystem'))
executable('shop-scanner-session', ['scanner-session/main.vala', 'scanner-session/scannersession.vala', 'scanner-session/scannersession-interface.vala', 'database/db-interface.vala', 'input-device/input-device-interface.vala', 'cli/cli-interface.vala', 'audio/audio-interface.vala', 'price.vapi'], dependencies: [gio_dep], install: true, install_dir: join_paths(get_option('libexecdir'), 'shopsystem'))
executable('shop-serial-device', ['serial-device/main.vala', 'serial-device/serial-device.vala', 'config/config-interface.vala'], dependencies: [gio_dep, linux_dep], install: true, install_dir: join_paths(get_option('libexecdir'), 'shopsystem'))
-executable('shop-web', ['web/main.vala', 'web/web.vala', 'web/websession.vala', 'web/csv.vala', 'web/template.vala', 'database/db-interface.vala', 'pgp/pgp-interface.vala', 'price.vapi', 'config/config-interface.vala', 'audio/audio-interface.vala'], dependencies: [gio_dep, gee_dep, soup_dep, posix_dep], install: true, install_dir: join_paths(get_option('libexecdir'), 'shopsystem'))
+executable('shop-web', ['web/main.vala', 'web/web.vala', 'web/websession.vala', 'web/csv.vala', 'web/template.vala', 'database/db-interface.vala', 'pgp/pgp-interface.vala', 'price.vapi', 'config/config-interface.vala', 'audio/audio-interface.vala', 'pdf-stock/pdf-stock-interface.vala'], dependencies: [gio_dep, gee_dep, soup_dep, posix_dep], install: true, install_dir: join_paths(get_option('libexecdir'), 'shopsystem'))
diff --git a/src/pdf-stock/pdf-stock-interface.vala b/src/pdf-stock/pdf-stock-interface.vala
index 5168250..87d9f61 100644
--- a/src/pdf-stock/pdf-stock-interface.vala
+++ b/src/pdf-stock/pdf-stock-interface.vala
@@ -15,5 +15,5 @@
[DBus (name = "io.mainframe.shopsystem.StockPDF")]
public interface PDFStock : Object {
- public abstract uint8[] generate() throws DBusError, IOError;
+ public abstract uint8[] generate(bool allProducts) throws DBusError, IOError;
}
diff --git a/src/pdf-stock/pdf-stock.vala b/src/pdf-stock/pdf-stock.vala
index 2edba88..6270c43 100644
--- a/src/pdf-stock/pdf-stock.vala
+++ b/src/pdf-stock/pdf-stock.vala
@@ -154,7 +154,7 @@ public class StockPDF {
return Cairo.Status.SUCCESS;
}
- public uint8[] generate() throws DBusError, IOError {
+ public uint8[] generate(bool allProducts) throws DBusError, IOError {
data = null;
var surface = new Cairo.PdfSurface.for_stream(pdf_write, a4w, a4h);
@@ -187,6 +187,9 @@ public class StockPDF {
render_table_header();
foreach(var p in stock) {
+ if (!allProducts && p.amount <= 0) {
+ continue;
+ }
render_table_row(p);
y += eanh + 6;
diff --git a/src/pdf-stock/test.vala b/src/pdf-stock/test.vala
index 2e35f9c..92b6232 100644
--- a/src/pdf-stock/test.vala
+++ b/src/pdf-stock/test.vala
@@ -16,7 +16,7 @@
public static int main(string args[]) {
try {
PDFStock stock = Bus.get_proxy_sync(BusType.SYSTEM, "io.mainframe.shopsystem.StockPDF", "/io/mainframe/shopsystem/stockpdf");
- var pdfdata = stock.generate();
+ var pdfdata = stock.generate(true);
FileUtils.set_contents("test.pdf", (string) pdfdata, pdfdata.length);
} catch(IOError e) {
error(_("IO Error: %s"), e.message);
diff --git a/src/web/main.vala b/src/web/main.vala
index 761bba9..4c89e40 100644
--- a/src/web/main.vala
+++ b/src/web/main.vala
@@ -18,6 +18,7 @@ public CSVMemberFile csvimport;
public PGP pgp;
public Config cfg;
public AudioPlayer audio;
+public PDFStock pdfStock;
string templatedir;
string? shortname;
@@ -35,6 +36,7 @@ public static int main(string[] args) {
pgp = Bus.get_proxy_sync(BusType.SYSTEM, "io.mainframe.shopsystem.PGP", "/io/mainframe/shopsystem/pgp");
cfg = Bus.get_proxy_sync(BusType.SYSTEM, "io.mainframe.shopsystem.Config", "/io/mainframe/shopsystem/config");
audio = Bus.get_proxy_sync(BusType.SYSTEM, "io.mainframe.shopsystem.AudioPlayer", "/io/mainframe/shopsystem/audio");
+ pdfStock = Bus.get_proxy_sync(BusType.SYSTEM, "io.mainframe.shopsystem.StockPDF", "/io/mainframe/shopsystem/stockpdf");
var datapath = cfg.get_string("GENERAL", "datapath");
templatedir = Path.build_filename(datapath, "templates");
port = cfg.get_integer("WEB", "port");
diff --git a/src/web/web.vala b/src/web/web.vala
index dbfcb54..3098308 100644
--- a/src/web/web.vala
+++ b/src/web/web.vala
@@ -748,6 +748,145 @@ public class WebServer {
}
}
+ void handler_stock_as_pdf(Soup.Server server, Soup.Message msg, string path, GLib.HashTable<string,string>? query, Soup.ClientContext client) {
+ try {
+ var session = new WebSession(server, msg, path, query, client);
+ if(!session.superuser && !session.auth_products) {
+ handler_403(server, msg, path, query, client);
+ return;
+ }
+
+ var allProducts = query.contains("all");
+
+ var pdfdata = pdfStock.generate(allProducts);
+ msg.set_status(200);
+ msg.set_response("application/pdf", Soup.MemoryUse.COPY, pdfdata);
+ } catch(Error e) {
+ handler_400(server, msg, path, query, client, e.message);
+ }
+ }
+
+ void handler_products_inventory(Soup.Server server, Soup.Message msg, string path, GLib.HashTable<string,string>? query, Soup.ClientContext client) {
+ try {
+ var session = new WebSession(server, msg, path, query, client);
+ var template = new WebTemplate("products/inventory.html", session);
+ template.replace("TITLE", "KtT Shop System: Inventory");
+ template.menu_set_active("products");
+
+ if(!session.superuser && !session.auth_products) {
+ handler_403(server, msg, path, query, client);
+ return;
+ }
+
+ string table = "";
+ string actionTemplate = "";
+ string restockClassTemplate = "hidden";
+ string suppliersTemplate = "";
+ string usersTemplate = "";
+ string successClassTemplate = "hidden";
+ if (msg.method == "POST") {
+ var postdata = Soup.Form.decode((string) msg.request_body.data);
+
+ if (!postdata.contains("apply_inventory")) {
+ // PUT / show changes and request an apply
+ foreach(var e in db.get_stock()) {
+ var realAmountStr = postdata.get(e.ean.to_string());
+ if (realAmountStr != null && realAmountStr.length > 0) {
+ var realAmount = int.parse(realAmountStr);
+ var amountStyleClass = "success";
+ if (realAmount < e.amount) {
+ amountStyleClass = "danger";
+ } else if (realAmount > e.amount) {
+ amountStyleClass = "info";
+ }
+ var diff = realAmount - e.amount;
+ table += @"<tr class='$(amountStyleClass)'><td>$(e.ean)</td><td>$(e.name)</td><td>$(e.category)</td><td>$(e.amount)</td><td>"
+ + @"$(realAmount) <strong>[ $(diff) ]</strong><input type=\"hidden\" name=\"$(e.ean)\" value=\"$(realAmount)\"></td></tr>";
+ }
+ }
+ actionTemplate = """<input type="hidden" name="apply_inventory" value="true"><button type="submit" class="btn btn-primary">Apply Changes</button>""";
+
+ // a list of suppliers to choose
+ suppliersTemplate = "<option value=\"0\">Unknown</option>";
+ foreach(var e in db.get_supplier_list()) {
+ suppliersTemplate += "<option value=\"%lld\">%s</option>".printf(e.id, e.name);
+ }
+
+ // a list of users to choose
+ foreach(var uId in db.get_system_member_ids()) {
+ var user = db.get_user_info(uId);
+ usersTemplate += "<option value=\"%d\">%s %s (%d)</option>".printf(uId, user.firstname, user.lastname, uId);
+ }
+
+ restockClassTemplate = ""; // this shows the option list
+ } else {
+ // PUT / apply changes
+
+ var supplierId = int.parse(postdata.get("supplierId"));
+ var userId = int.parse(postdata.get("userId"));
+ foreach(var e in db.get_stock()) {
+ var realAmountStr = postdata.get(e.ean.to_string());
+ if (realAmountStr != null && realAmountStr.length > 0) {
+ var pId = uint64.parse(e.ean.to_string());
+ var realAmount = int.parse(realAmountStr);
+ if (realAmount < e.amount) {
+ // Loss transaction
+
+ for (int i=0; i< e.amount - realAmount; i++) {
+ db.buy(userId, pId);
+ }
+ } else if (realAmount > e.amount) {
+ // Restock
+
+ var amountDiff = realAmount - e.amount;
+ // find the latest bbd date
+ int64 maxBbd = 0;
+ foreach(var restock in db.get_restocks(pId, true)) {
+ if (restock.best_before_date > maxBbd) {
+ maxBbd = restock.best_before_date;
+ }
+ }
+
+ //stderr.printf("restock %d for %d, diff %d, supplier %d\n", (int)maxBbd, (int)pId, amountDiff, supplierId);
+ db.restock(session.user, pId, amountDiff, 0, supplierId, maxBbd);
+ }
+ }
+ }
+
+ msg.set_redirect(302, "/products/inventory?success=x");
+ return;
+ }
+ } else {
+ // default GET / list products with a form
+ var tabindexCounter = 1;
+ foreach(var e in db.get_stock()) {
+ table += @"<tr><td><a href=\"/products/$(e.ean)\">$(e.ean)</a></td><td><a href=\"/products/$(e.ean)\">$(e.name)</a></td><td>$(e.category)</td><td>$(e.amount)</td><td><input type=\"number\" name=\"$(e.ean)\" tabindex=\"$(tabindexCounter)\"></td></tr>";
+ tabindexCounter++;
+ }
+ actionTemplate = """<button type="submit" class="btn btn-primary">Preview</button>""";
+
+ if (query.contains("success")) {
+ successClassTemplate = "";
+ }
+ }
+
+ template.replace("SUCESS_HIDDEN", successClassTemplate);
+ template.replace("USERS", usersTemplate);
+ template.replace("SUPPLIERS", suppliersTemplate);
+ template.replace("RESTOCK_HIDDEN", restockClassTemplate);
+ template.replace("DATA", table);
+ template.replace("ACTION", actionTemplate);
+ msg.set_response("text/html", Soup.MemoryUse.COPY, template.data);
+ msg.set_status(200);
+ return;
+ } catch(TemplateError e) {
+ stderr.printf(e.message+"\n");
+ handler_404(server, msg, path, query, client);
+ } catch(Error e) {
+ handler_400(server, msg, path, query, client, e.message);
+ }
+ }
+
void handler_products_new(Soup.Server server, Soup.Message msg, string path, GLib.HashTable<string,string>? query, Soup.ClientContext client) {
try {
var session = new WebSession(server, msg, path, query, client);
@@ -1486,6 +1625,8 @@ public class WebServer {
srv.add_handler("/products", handler_products);
srv.add_handler("/products/new", handler_products_new);
srv.add_handler("/products/bestbefore", handler_product_bestbefore);
+ srv.add_handler("/products/inventory", handler_products_inventory);
+ srv.add_handler("/products/inventory-pdf", handler_stock_as_pdf);
srv.add_handler("/aliases", handler_alias_list);
srv.add_handler("/aliases/new", handler_alias_new);