diff options
-rw-r--r-- | README.md | 6 | ||||
-rw-r--r-- | data/templates/css/base.css | 4 | ||||
-rw-r--r-- | data/templates/menu.html | 18 | ||||
-rw-r--r-- | data/templates/products/entry.html | 41 | ||||
-rw-r--r-- | data/templates/products/index.html | 11 | ||||
-rw-r--r-- | data/templates/products/inventory.html | 49 | ||||
-rw-r--r-- | docker/Dockerfile | 1 | ||||
-rwxr-xr-x | docker/init.sh | 2 | ||||
-rw-r--r-- | src/database/database.vala | 11 | ||||
-rw-r--r-- | src/database/db-interface.vala | 3 | ||||
-rw-r--r-- | src/meson.build | 2 | ||||
-rw-r--r-- | src/pdf-stock/pdf-stock-interface.vala | 2 | ||||
-rw-r--r-- | src/pdf-stock/pdf-stock.vala | 5 | ||||
-rw-r--r-- | src/pdf-stock/test.vala | 2 | ||||
-rw-r--r-- | src/web/main.vala | 2 | ||||
-rw-r--r-- | src/web/web.vala | 141 |
16 files changed, 283 insertions, 17 deletions
@@ -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); |