From bb55e121576a5b5d225bfc68c5062f386cc32db9 Mon Sep 17 00:00:00 2001 From: Holger Cremer Date: Thu, 26 Jul 2018 20:09:26 +0200 Subject: adds an inventory function --- README | 8 +- docker/init.sh | 2 + src/database/database.vala | 11 +++ src/database/db-interface.vala | 1 + src/pdf-stock/Makefile | 4 +- src/pdf-stock/pdf-stock-interface.vala | 3 +- src/pdf-stock/pdf-stock.vala | 5 +- src/web/Makefile | 2 +- src/web/main.vala | 11 ++- src/web/web.vala | 145 +++++++++++++++++++++++++++++++++ templates/css/base.css | 4 + templates/menu.html | 10 ++- templates/products/index.html | 4 +- templates/products/inventory.html | 49 +++++++++++ 14 files changed, 245 insertions(+), 14 deletions(-) create mode 100644 templates/products/inventory.html diff --git a/README b/README index 9741ee2..a656f4d 100644 --- a/README +++ b/README @@ -78,4 +78,10 @@ but you need to modify a few things. * Create user `sqlite3 shop.db "INSERT INTO users (id, email, firstname, lastname) VALUES (1, "test@tester", "Firstname", "Lastname");` * Setup user password - sqlite3 shop.db "update authentication set password = '$(echo -n yourpw | shasum -a 256 | cut -d " " -f 1)' where user = 1;" \ No newline at end of file + sqlite3 shop.db "update authentication set password = '$(echo -n yourpw | shasum -a 256 | cut -d " " -f 1)' where user = 1;" + +=== 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/docker/init.sh b/docker/init.sh index 15b7741..49a3aa1 100755 --- a/docker/init.sh +++ b/docker/init.sh @@ -5,6 +5,8 @@ make install cd config make install +export LD_LIBRARY_PATH=/mnt/serial-barcode-scanner/libcairobarcode + cd ../.. dbus-daemon --system echo "Ready!" diff --git a/src/database/database.vala b/src/database/database.vala index fb7bde6..0d3d1ff 100644 --- a/src/database/database.vala +++ b/src/database/database.vala @@ -121,6 +121,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 = ?"; @@ -841,6 +842,16 @@ public class DataBase : Object { return result; } + public int[] get_system_member_ids() { + 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 DatabaseError { int rc; diff --git a/src/database/db-interface.vala b/src/database/db-interface.vala index bd94ce1..ac8c215 100644 --- a/src/database/db-interface.vala +++ b/src/database/db-interface.vala @@ -44,6 +44,7 @@ public interface Database : Object { public abstract int64 get_last_purchase(int user) throws IOError; public abstract StatsInfo get_stats_info() throws IOError; public abstract int[] get_member_ids() throws IOError; + public abstract int[] get_system_member_ids() throws IOError; public abstract void user_disable(int user, bool value) throws IOError, DatabaseError; public abstract void user_replace(UserInfo u) throws IOError, DatabaseError; public abstract bool user_is_disabled(int user) throws IOError, DatabaseError; diff --git a/src/pdf-stock/Makefile b/src/pdf-stock/Makefile index 2c8be58..a9ad1a6 100644 --- a/src/pdf-stock/Makefile +++ b/src/pdf-stock/Makefile @@ -6,10 +6,10 @@ all: pdf-stock @echo > /dev/null pdf-stock: main.vala pdf-stock.vala ../database/db-interface.vala ../price.vapi ../../libcairobarcode/libcairobarcode.vapi - ${VALAC} -X -w ${LIBCAIROBARCODE} -o $@ --pkg cairo --pkg pangocairo --pkg gio-2.0 --pkg posix $^ + valac -X -w ${LIBCAIROBARCODE} -o $@ --pkg cairo --pkg pangocairo --pkg gio-2.0 --pkg posix $^ test: test.vala pdf-stock-interface.vala - ${VALAC} -X -w -o $@ --pkg gio-2.0 $^ + valac -X -w -o $@ --pkg gio-2.0 $^ run: pdf-stock LD_LIBRARY_PATH=../../libcairobarcode ./pdf-stock diff --git a/src/pdf-stock/pdf-stock-interface.vala b/src/pdf-stock/pdf-stock-interface.vala index 415916f..be49a20 100644 --- a/src/pdf-stock/pdf-stock-interface.vala +++ b/src/pdf-stock/pdf-stock-interface.vala @@ -15,5 +15,6 @@ [DBus (name = "io.mainframe.shopsystem.StockPDF")] public interface PDFStock : Object { - public abstract uint8[] generate() throws IOError; + // if false, only products with an amount > 0 are selected + public abstract uint8[] generate(bool allProducts) throws IOError; } diff --git a/src/pdf-stock/pdf-stock.vala b/src/pdf-stock/pdf-stock.vala index e971f78..ac51c23 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() { + public uint8[] generate(bool allProducts) { 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/web/Makefile b/src/web/Makefile index e6094f6..94aab97 100644 --- a/src/web/Makefile +++ b/src/web/Makefile @@ -1,7 +1,7 @@ all: web @echo > /dev/null -web: main.vala web.vala websession.vala csv.vala template.vala ../database/db-interface.vala ../pgp/pgp-interface.vala ../price.vapi ../config/config-interface.vala ../audio/audio-interface.vala +web: main.vala web.vala websession.vala csv.vala 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 valac -X -w -o $@ --vapidir=../../vapi --enable-experimental --pkg gee-0.8 --pkg gio-2.0 --pkg libsoup-2.4 --pkg posix $^ clean: diff --git a/src/web/main.vala b/src/web/main.vala index 7070e66..aefe7fd 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; public static int main(string[] args) { @@ -27,10 +28,12 @@ public static int main(string[] args) { uint port = 8080; try { - db = Bus.get_proxy_sync(BusType.SYSTEM, "io.mainframe.shopsystem.Database", "/io/mainframe/shopsystem/database"); - 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"); + db = Bus.get_proxy_sync(BusType.SYSTEM, "io.mainframe.shopsystem.Database", "/io/mainframe/shopsystem/database"); + 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"); + templatedir = cfg.get_string("WEB", "filepath"); port = cfg.get_integer("WEB", "port"); diff --git a/src/web/web.vala b/src/web/web.vala index ea0c667..5a44409 100644 --- a/src/web/web.vala +++ b/src/web/web.vala @@ -721,6 +721,149 @@ public class WebServer { } } + void handler_stock_as_pdf(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? 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(DatabaseError e) { + handler_400(server, msg, path, query, client, e.message); + } catch(IOError e) { + handler_400(server, msg, path, query, client, e.message); + } + } + + void handler_products_inventory(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? 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.id); + if (realAmountStr != null && realAmountStr.length > 0) { + var realAmount = int.parse(realAmountStr); + var amountStyleClass = "success"; + if (realAmount < e.amount) { + amountStyleClass = "error"; + } else if (realAmount > e.amount) { + amountStyleClass = "info"; + } + var diff = realAmount - e.amount; + table += @"$(e.id)$(e.name)$(e.category)$(e.amount)" + + @"$(realAmount) [ $(diff) ]"; + } + } + actionTemplate = """"""; + + // a list of suppliers to choose + suppliersTemplate = ""; + foreach(var e in db.get_supplier_list()) { + suppliersTemplate += "".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 += "".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.id); + if (realAmountStr != null && realAmountStr.length > 0) { + var pId = uint64.parse(e.id); + 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 += @"$(e.id)$(e.name)$(e.category)$(e.amount)"; + tabindexCounter++; + } + actionTemplate = """"""; + + 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(DatabaseError e) { + handler_400(server, msg, path, query, client, e.message); + } catch(IOError e) { + handler_400(server, msg, path, query, client, e.message); + } + } + void handler_products_new(Soup.Server server, Soup.Message msg, string path, GLib.HashTable? query, Soup.ClientContext client) { try { var session = new WebSession(server, msg, path, query, client); @@ -1417,6 +1560,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); diff --git a/templates/css/base.css b/templates/css/base.css index e273808..2444eca 100644 --- a/templates/css/base.css +++ b/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/templates/menu.html b/templates/menu.html index 764068a..1e73a8c 100644 --- a/templates/menu.html +++ b/templates/menu.html @@ -3,7 +3,15 @@