Typica is a free program for professional coffee roasters. https://typica.us
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

navigation.xml 27KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. <window id="navwindow">
  2. <layout type="grid">
  3. <row>
  4. <column>
  5. <button name="Configure Roasters" id="configure" type="push" />
  6. </column>
  7. </row>
  8. <row>
  9. <column>
  10. <sqldrop id="machineselector" />
  11. </column>
  12. <column>
  13. <button name="Roast Coffee" id="roast" type="push" />
  14. </column>
  15. </row>
  16. <row>
  17. <column>
  18. <button name="Manual Roasting Log Entry" id="manual" type="push" />
  19. </column>
  20. </row>
  21. <row>
  22. <column>
  23. <button name="Purchase Green Coffee" id="green" type="push" />
  24. </column>
  25. </row>
  26. <row>
  27. <column>
  28. <button name="Manage Roasted Coffee Items" id="newroasted" type="push" />
  29. </column>
  30. </row>
  31. <row>
  32. <column>
  33. <button name="Edit Roasting Specification" id="roastspec" type="push" />
  34. </column>
  35. </row>
  36. <row>
  37. <column>
  38. <button name="Update Inventory" id="inventory" type="push" />
  39. </column>
  40. </row>
  41. <row>
  42. <column>
  43. <button name="New Cupping Session" id="createcupping" type="push" />
  44. </column>
  45. </row>
  46. <row>
  47. <column>
  48. <button name="Join Cupping Session" id="joincupping" type="push" />
  49. </column>
  50. </row>
  51. <row>
  52. <column>
  53. <button name="Summarize Cupping Session" id="sumcupping" type="push" />
  54. </column>
  55. </row>
  56. <row>
  57. <column>
  58. <button name="View Target Roast Profiles" id="profilehistory" type="push" />
  59. </column>
  60. </row>
  61. <row>
  62. <column>
  63. <button name="Import Target Roast Profiles" id="target" type="push" />
  64. </column>
  65. </row>
  66. <row>
  67. <column>
  68. <button name="Enter Green Coffee Sales" id="greensales" type="push" />
  69. </column>
  70. </row>
  71. </layout>
  72. <menu name="Reports" type="reports" src="Reports" />
  73. <menu name="Database">
  74. <item id="resetconnection">Forget Connection Details</item>
  75. </menu>
  76. <menu name="Users">
  77. <item id="switchuser">Switch User</item>
  78. <item id="createuser">Create New Users</item>
  79. </menu>
  80. <program>
  81. var window = this;
  82. var navigationwindow = window;
  83. window.loggingWindow = undefined;
  84. var roasterlist = findChildObject(this, 'machineselector');
  85. var model = new DeviceTreeModel;
  86. roasterlist.setModel(model);
  87. roasterlist.currentIndex = QSettings.value("machineSelection", 0);
  88. roasterlist['currentIndexChanged(int)'].connect(function() {
  89. QSettings.setValue("machineSelection", roasterlist.currentIndex);
  90. });
  91. var resetdbconnection = findChildObject(this, 'resetconnection');
  92. resetdbconnection.triggered.connect(function() {
  93. QSettings.setValue("database/exists", false);
  94. QSettings.setValue("database/hostname", "");
  95. QSettings.setValue("database/dbname", "");
  96. QSettings.setValue("database/user", "");
  97. QSettings.setValue("database/password", "");
  98. });
  99. var manual = findChildObject(this, 'manual');
  100. manual.clicked.connect(function() {
  101. createWindow("manualLogEntry");
  102. });
  103. var profilehistory = findChildObject(this, 'profilehistory');
  104. profilehistory.clicked.connect(function() {
  105. createWindow("profilehistory");
  106. });
  107. var greensalesbutton = findChildObject(this, 'greensales');
  108. greensalesbutton.clicked.connect(function() {
  109. createWindow("greensales");
  110. });
  111. var sumcup = findChildObject(this, 'sumcupping');
  112. sumcup.clicked.connect(function() {
  113. var sessionlist = createWindow("finsessionlist");
  114. sessionlist.windowTitle = "Typica - Summarize Cupping Session";
  115. });
  116. var ncsbutton = findChildObject(this, 'createcupping')
  117. ncsbutton.clicked.connect(function() {
  118. var ncswindow = createWindow("session");
  119. ncswindow.windowTitle = "Typica - New Cupping Session";
  120. });
  121. var jcsbutton = findChildObject(this, 'joincupping')
  122. jcsbutton.clicked.connect(function() {
  123. var jcswindow = createWindow("sessionlist");
  124. jcswindow.windowTitle = "Typica - Join Cupping Session";
  125. });
  126. /*
  127. var nrbutton = findChildObject(this, 'newroaster');
  128. nrbutton.clicked.connect(function() {
  129. var nrwindow = createWindow("newroaster");
  130. nrwindow.windowTitle = "Typica - New Roaster";
  131. });
  132. */
  133. var inventory = findChildObject(this, 'inventory');
  134. inventory.clicked.connect(function() {
  135. var invwin = createWindow("inventory");
  136. invwin.windowTitle = "Typica - Inventory";
  137. });
  138. var roastspecbutton = findChildObject(this, 'roastspec');
  139. roastspecbutton.clicked.connect(function() {
  140. var specwindow = createWindow("roastspec");
  141. });
  142. var gbutton = findChildObject(this, 'green');
  143. gbutton.clicked.connect(function() {
  144. var purchasewindow = createWindow("purchase");
  145. });
  146. var nrbutton = findChildObject(this, 'newroasted');
  147. nrbutton.clicked.connect(function() {
  148. var nrwindow = createWindow("newroasted");
  149. nrwindow.windowTitle = "Manage Roasted Coffee Items";
  150. });
  151. var importb = findChildObject(this, 'target');
  152. importb.clicked.connect(function() {
  153. var importWindow = createWindow("importTargets");
  154. importWindow.windowTitle = "Typica - Import Target Roast Profiles";
  155. });
  156. var roastbutton = findChildObject(this, 'roast');
  157. roastbutton.clicked.connect(function() {
  158. if(typeof(window.loggingWindow) == "undefined")
  159. {
  160. window.loggingWindow = createWindow("basicWindow");
  161. window.loggingWindow.windowTitle = "Typica [*]";
  162. window.loggingWindow.navigationWindow = window;
  163. }
  164. else
  165. {
  166. window.loggingWindow.raise();
  167. window.loggingWindow.activateWindow();
  168. }
  169. });
  170. var configurebutton = findChildObject(this, 'configure');
  171. configurebutton.clicked.connect(function() {
  172. var confwindow = new SettingsWindow;
  173. confwindow.show();
  174. });
  175. <![CDATA[
  176. var DBCreateBase = function() {
  177. var query = new QSqlQuery();
  178. query.exec("CREATE TABLE IF NOT EXISTS certifications (item bigint NOT NULL, certification text NOT NULL)");
  179. query.exec("CREATE TABLE IF NOT EXISTS cupping_samples (session bigint NOT NULL, sample text NOT NULL, position bigint NOT NULL, type text NOT NULL, \"time\" timestamp without time zone, machine bigint, point text, item bigint)");
  180. query.exec("CREATE TABLE cupping_sessions (id bigserial NOT NULL, event text, name text NOT NULL, \"time\" timestamp without time zone NOT NULL, blind boolean NOT NULL, open boolean NOT NULL, note text)");
  181. query.exec("CREATE TABLE IF NOT EXISTS cuppingforms (session bigint NOT NULL, sample text NOT NULL, position bigint NOT NULL, grader text, finalscore numeric, notes text, serialization text)");
  182. query.exec("CREATE TABLE IF NOT EXISTS cuppingform_t1 (aroma numeric, flavor numeric, aftertaste numeric, acidity numeric, body numeric, uniformity numeric, balance numeric, cleancup numeric, sweetness numeric, overall numeric, total numeric) INHERITS (cuppingforms)");
  183. query.exec("CREATE TABLE IF NOT EXISTS invoices (id bigserial PRIMARY KEY NOT NULL, invoice text, vendor text NOT NULL, \"time\" timestamp without time zone NOT NULL)");
  184. query.exec("CREATE TABLE IF NOT EXISTS invoice_items (invoice_id bigint NOT NULL, record_type text NOT NULL, item_id bigint, description text NOT NULL, cost numeric NOT NULL)");
  185. query.exec("CREATE TABLE IF NOT EXISTS transactions (\"time\" timestamp without time zone NOT NULL, item bigint NOT NULL)");
  186. query.exec("CREATE TABLE IF NOT EXISTS inventory (quantity numeric NOT NULL) INHERITS (transactions)");
  187. query.exec("CREATE TABLE IF NOT EXISTS loss (quantity numeric NOT NULL, reason text) INHERITS (transactions)");
  188. query.exec("CREATE TABLE IF NOT EXISTS make (quantity numeric NOT NULL) INHERITS (transactions)");
  189. query.exec("CREATE TABLE IF NOT EXISTS purchase (quantity numeric NOT NULL, cost numeric NOT NULL, vendor text NOT NULL) INHERITS (transactions)");
  190. query.exec("CREATE TABLE IF NOT EXISTS sale (quantity numeric NOT NULL, customer text) INHERITS (transactions)");
  191. query.exec("CREATE TABLE IF NOT EXISTS use (quantity numeric NOT NULL) INHERITS (transactions)");
  192. query.exec("CREATE VIEW all_transactions AS ((((SELECT purchase.\"time\", purchase.item, purchase.quantity, purchase.cost, purchase.vendor, NULL::unknown AS reason, NULL::unknown AS customer, 'PURCHASE' AS type FROM purchase UNION SELECT use.\"time\", use.item, use.quantity, NULL::unknown AS cost, NULL::unknown AS vendor, NULL::unknown AS reason, NULL::unknown AS customer, 'USE' AS type FROM use) UNION SELECT inventory.\"time\", inventory.item, inventory.quantity, NULL::unknown AS cost, NULL::unknown AS vendor, NULL::unknown AS reason, NULL::unknown AS customer, 'INVENTORY' AS type FROM inventory) UNION SELECT loss.\"time\", loss.item, loss.quantity, NULL::unknown AS cost, NULL::unknown AS vendor, loss.reason, NULL::unknown AS customer, 'LOSS' AS type FROM loss) UNION SELECT make.\"time\", make.item, make.quantity, NULL::unknown AS cost, NULL::unknown AS vendor, NULL::unknown AS reason, NULL::unknown AS customer, 'MAKE' AS type FROM make) UNION SELECT sale.\"time\", sale.item, sale.quantity, NULL::unknown AS cost, NULL::unknown AS vendor, NULL::unknown AS reason, sale.customer, 'SALE' AS type FROM sale");
  193. query.exec("CREATE FUNCTION time_range(bigint) RETURNS integer AS $$ BEGIN IF (SELECT quantity FROM items WHERE id = $1) > 0 THEN RETURN (SELECT current_date - min(time)::date + 1 FROM use WHERE item = $1); ELSE RETURN (SELECT max(time)::date - min(time)::date + 1 FROM use WHERE item = $1); END IF; END; $$ LANGUAGE plpgsql STRICT");
  194. query.exec("CREATE TABLE IF NOT EXISTS items(id bigint NOT NULL, name text NOT NULL, reference text, unit text NOT NULL, quantity numeric DEFAULT 0, category text)");
  195. query.exec("CREATE SEQUENCE items_id_seq INCREMENT BY 1 NO MAXVALUE NO MINVALUE CACHE 1");
  196. query.exec("CREATE TABLE IF NOT EXISTS coffees(origin text NOT NULL, region text, producer text, grade text, milling text, drying text) INHERITS (items)");
  197. query.exec("CREATE VIEW coffee_history AS SELECT coffees.id, coffees.name, coffees.origin, coffees.quantity AS stock, (SELECT sum(use.quantity) AS sum FROM use WHERE (use.item = coffees.id)) AS used, time_range(coffees.id) AS \"interval\", ((SELECT (sum(use.quantity) / (time_range(use.item))::numeric) FROM use WHERE (use.item = coffees.id) GROUP BY use.item))::numeric(10,2) AS rate, (SELECT (('now'::text)::date + ((coffees.quantity / (SELECT (sum(use.quantity) / (time_range(use.item))::numeric) FROM use WHERE (use.item = coffees.id) GROUP BY use.item)))::integer)) AS \"out\" FROM coffees WHERE (coffees.id IN (SELECT use.item FROM use)) ORDER BY coffees.origin");
  198. query.exec("CREATE TABLE IF NOT EXISTS current_items (item bigint NOT NULL)");
  199. query.exec("CREATE TABLE IF NOT EXISTS decaf_coffees (decaf_method text NOT NULL) INHERITS (coffees)");
  200. query.exec("CREATE TABLE IF NOT EXISTS files (id bigint NOT NULL, name text NOT NULL, type text NOT NULL, note text, file bytea NOT NULL)");
  201. query.exec("CREATE TABLE IF NOT EXISTS item_files(\"time\" timestamp without time zone NOT NULL, item bigint NOT NULL, files bigint[] NOT NULL)");
  202. query.exec("CREATE TYPE item_transaction_with_balance AS (\"time\" timestamp without time zone, item bigint, quantity numeric, cost numeric, vendor text, reason text, customer text, type text, balance numeric)");
  203. query.exec("CREATE TABLE IF NOT EXISTS lb_bag_conversion (item bigint NOT NULL, conversion numeric NOT NULL)");
  204. query.exec("CREATE TABLE IF NOT EXISTS machine (id bigint NOT NULL, name text NOT NULL)");
  205. query.exec("CREATE VIEW regular_coffees AS SELECT coffees.id, coffees.name, coffees.reference, coffees.unit, coffees.quantity, coffees.category, coffees.origin, coffees.region, coffees.producer, coffees.grade, coffees.milling, coffees.drying FROM coffees WHERE (NOT (coffees.id IN (SELECT decaf_coffees.id FROM decaf_coffees)))");
  206. query.exec("CREATE TABLE IF NOT EXISTS roasting_log (\"time\" timestamp without time zone NOT NULL, unroasted_id bigint[], unroasted_quantity numeric[], unroasted_total_quantity numeric, roasted_id bigint, roasted_quantity numeric, transaction_type text NOT NULL, annotation text, machine bigint NOT NULL, duration interval, approval boolean, humidity numeric, barometric numeric, indoor_air numeric, outdoor_air numeric, files bigint[])");
  207. query.exec("CREATE VIEW short_log AS SELECT roasting_log.\"time\", (SELECT items.name FROM items WHERE (items.id = roasting_log.roasted_id)) AS name, roasting_log.unroasted_total_quantity, roasting_log.roasted_quantity, ((((roasting_log.unroasted_total_quantity - roasting_log.roasted_quantity) / roasting_log.unroasted_total_quantity) * (100)::numeric))::numeric(12,2) AS weight_loss, roasting_log.duration FROM roasting_log ORDER BY roasting_log.\"time\"");
  208. query.exec("CREATE FUNCTION add_inventory() RETURNS trigger AS $$ BEGIN UPDATE items SET quantity = quantity + NEW.quantity WHERE id = NEW.item; RETURN NEW; END; $$ LANGUAGE plpgsql");
  209. query.exec("CREATE FUNCTION bags_in_stock(bigint) RETURNS numeric AS $_$SELECT quantity / (SELECT conversion FROM lb_bag_conversion WHERE item = id) FROM items WHERE id = $1;$_$ LANGUAGE sql IMMUTABLE STRICT");
  210. query.exec("CREATE FUNCTION log_use() RETURNS trigger AS $$ DECLARE i integer := array_lower(NEW.unroasted_id, 1); u integer := array_upper(NEW.unroasted_id, 1); BEGIN WHILE i <= u LOOP INSERT INTO use VALUES(NEW.time, NEW.unroasted_id[i], NEW.unroasted_quantity[i]); i := i + 1; END LOOP; RETURN NEW; END; $$ LANGUAGE plpgsql");
  211. query.exec("CREATE FUNCTION replace_inventory() RETURNS trigger AS $$ BEGIN UPDATE items SET quantity = NEW.quantity WHERE id = NEW.item; RETURN NEW; END; $$ LANGUAGE plpgsql");
  212. query.exec("CREATE FUNCTION subtract_inventory() RETURNS trigger AS $$ BEGIN UPDATE items SET quantity = quantity - NEW.quantity WHERE id = NEW.item; RETURN NEW; END; $$ LANGUAGE plpgsql");
  213. query.exec("CREATE SEQUENCE files_id_seq START WITH 1 INCREMENT BY 1 NO MAXVALUE NO MINVALUE CACHE 1");
  214. query.exec("ALTER TABLE files ALTER COLUMN id SET DEFAULT nextval('files_id_seq'::regclass)");
  215. query.exec("ALTER TABLE items ALTER COLUMN id SET DEFAULT nextval('items_id_seq'::regclass)");
  216. query.exec("ALTER TABLE ONLY files ADD CONSTRAINT file_pkey PRIMARY KEY (id)");
  217. query.exec("ALTER TABLE ONLY items ADD CONSTRAINT items_pkey PRIMARY KEY (id)");
  218. query.exec("ALTER TABLE ONLY lb_bag_conversion ADD CONSTRAINT lb_bag_conversion_item_key UNIQUE (item)");
  219. query.exec("ALTER TABLE ONLY roasting_log ADD CONSTRAINT roasting_log_pkey PRIMARY KEY (\"time\", machine)");
  220. query.exec("CREATE INDEX itemcategories ON items USING btree (category)");
  221. query.exec("CREATE INDEX itemnames ON items USING btree (name)");
  222. query.exec("CREATE INDEX roasting_log_index ON roasting_log USING btree (\"time\")");
  223. query.exec("CREATE INDEX transactionitems ON transactions USING btree (item)");
  224. query.exec("CREATE INDEX transactiontimes ON transactions USING btree (\"time\")");
  225. query.exec("CREATE TRIGGER add_inventory_trigger AFTER INSERT ON purchase FOR EACH ROW EXECUTE PROCEDURE add_inventory()");
  226. query.exec("CREATE TRIGGER add_inventory_trigger AFTER INSERT ON make FOR EACH ROW EXECUTE PROCEDURE add_inventory()");
  227. query.exec("CREATE TRIGGER log_use_trigger AFTER INSERT ON roasting_log FOR EACH ROW EXECUTE PROCEDURE log_use()");
  228. query.exec("CREATE TRIGGER replace_inventory_trigger AFTER INSERT ON inventory FOR EACH ROW EXECUTE PROCEDURE replace_inventory()");
  229. query.exec("CREATE TRIGGER subtract_inventory_trigger AFTER INSERT ON loss FOR EACH ROW EXECUTE PROCEDURE subtract_inventory()");
  230. query.exec("CREATE TRIGGER subtract_inventory_trigger AFTER INSERT ON sale FOR EACH ROW EXECUTE PROCEDURE subtract_inventory()");
  231. query.exec("CREATE TRIGGER subtract_inventory_trigger AFTER INSERT ON use FOR EACH ROW EXECUTE PROCEDURE subtract_inventory()");
  232. query.exec("ALTER TABLE ONLY item_files ADD CONSTRAINT item_files_item_fkey FOREIGN KEY (item) REFERENCES items(id)");
  233. query.exec("ALTER TABLE ONLY transactions ADD CONSTRAINT transactions_item_fkey FOREIGN KEY (item) REFERENCES items(id)");
  234. query.exec("INSERT INTO TypicaFeatures (feature, enabled, version) VALUES('base-features', TRUE, 1)");
  235. query = query.invalidate();
  236. };
  237. /* Some changes to the database are required for sample roasting features in
  238. Typica 1.6 and later. */
  239. var DBCreateSampleRoasting = function() {
  240. var query = new QSqlQuery;
  241. query.exec("CREATE TABLE IF NOT EXISTS item_attributes (id bigserial PRIMARY KEY NOT NULL, name text NOT NULL)");
  242. query.exec("CREATE TABLE IF NOT EXISTS coffee_sample_items(arrival timestamp without time zone, vendor text, attribute_ids bigint[], attribute_values text[], item_id bigint) INHERITS (items)");
  243. query.exec("INSERT INTO TypicaFeatures (feature, enabled, version) VALUES('sample-roasting', TRUE, 1)");
  244. query = query.invalidate();
  245. };
  246. /* Some changes to the database are required to log who performed what tasks.
  247. This adds logging for all transaction types and also for the roasting log.*/
  248. var DBUpdateMultiUser = function() {
  249. var query = new QSqlQuery;
  250. query.exec("ALTER TABLE transactions ADD COLUMN person text DEFAULT NULL");
  251. query.exec("ALTER TABLE roasting_log ADD COLUMN person text DEFAULT NULL");
  252. query.exec("CREATE OR REPLACE FUNCTION log_session_user() RETURNS trigger AS $$ BEGIN NEW.person := session_user; RETURN NEW; END; $$ LANGUAGE plpgsql");
  253. query.exec("CREATE TRIGGER log_person BEFORE INSERT ON inventory FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
  254. query.exec("CREATE TRIGGER log_person BEFORE INSERT ON loss FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
  255. query.exec("CREATE TRIGGER log_person BEFORE INSERT ON make FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
  256. query.exec("CREATE TRIGGER log_person BEFORE INSERT ON purchase FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
  257. query.exec("CREATE TRIGGER log_person BEFORE INSERT ON sale FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
  258. query.exec("CREATE TRIGGER log_person BEFORE INSERT ON use FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
  259. query.exec("CREATE TRIGGER log_person BEFORE INSERT ON roasting_log FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
  260. query.exec("UPDATE TypicaFeatures SET version = 2 WHERE feature = 'base-features'");
  261. query = query.invalidate();
  262. };
  263. /* Bug fix and optimization for item_history */
  264. var DBUpdateHistory = function() {
  265. var query = new QSqlQuery;
  266. query.exec("CREATE TYPE transaction_type AS (type text, quantity numeric)");
  267. query.exec("CREATE FUNCTION update_balance(numeric, transaction_type) RETURNS numeric AS $$ BEGIN CASE $2.type WHEN 'PURCHASE', 'MAKE' THEN RETURN $1 + $2.quantity; WHEN 'INVENTORY' THEN RETURN $2.quantity; WHEN 'USE', 'SALE', 'LOSS' THEN RETURN $1 - $2.quantity; END CASE; END; $$ LANGUAGE plpgsql STRICT");
  268. query.exec("CREATE AGGREGATE transaction_balance (BASETYPE = transaction_type, SFUNC = update_balance, STYPE = numeric, INITCOND = '0')");
  269. query.exec("CREATE OR REPLACE FUNCTION item_history(bigint) RETURNS SETOF item_transaction_with_balance AS $$ SELECT time, item, quantity, cost, vendor, reason, customer, type, transaction_balance((type, quantity)::transaction_type) OVER (PARTITION BY item ORDER BY time ASC) AS balance FROM all_transactions WHERE item = $1; $$ LANGUAGE SQL");
  270. query.exec("DROP FUNCTION calculate_inventory_balance()");
  271. query = query.invalidate();
  272. };
  273. /* Asynchronous notifications */
  274. var DBUpdateNotifications = function() {
  275. var query = new QSqlQuery;
  276. query.exec("CREATE OR REPLACE FUNCTION notify_roasting_log_change() RETURNS TRIGGER AS $$ BEGIN NOTIFY RoastingLogChange; RETURN NULL; END; $$ LANGUAGE plpgsql");
  277. query.exec("CREATE TRIGGER notify_roasting_log_change AFTER INSERT OR UPDATE ON roasting_log FOR EACH STATEMENT EXECUTE PROCEDURE notify_roasting_log_change()");
  278. query = query.invalidate();
  279. };
  280. /* Update trigger functions to make column names explicit */
  281. var DBUpdateTriggers = function() {
  282. var query = new QSqlQuery;
  283. query.exec("CREATE OR REPLACE FUNCTION log_make_update() RETURNS trigger AS $$ BEGIN IF NEW.roasted_quantity <> OLD.roasted_quantity AND NEW.roasted_quantity IS NOT NULL THEN INSERT INTO make (time, item, quantity) VALUES(NEW.time, NEW.roasted_id, NEW.roasted_quantity); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql");
  284. query.exec("UPDATE TypicaFeatures SET version = 4 WHERE feature = 'base-features'");
  285. };
  286. var DBUpdateReminders = function() {
  287. var query = new QSqlQuery;
  288. query.exec("CREATE TABLE IF NOT EXISTS reminders (id bigserial PRIMARY KEY NOT NULL, reminder text NOT NULL)");
  289. query.exec("UPDATE TypicaFeatures SET version = 5 WHERE feature = 'base-features'");
  290. query = query.invalidate();
  291. };
  292. var DBUpdateSpecification = function() {
  293. var query = new QSqlQuery;
  294. query.exec("CREATE TABLE IF NOT EXISTS roasting_specification (\"time\" timestamp without time zone NOT NULL, item bigint NOT NULL, loss numeric, tolerance numeric, notes text)");
  295. query.exec("UPDATE TypicaFeatures SET version = 6 WHERE feature = 'base-features'");
  296. query = query.invalidate();
  297. };
  298. /* Updates for Typica version 1.8 */
  299. var DBUpdate18 = function() {
  300. var query = new QSqlQuery;
  301. /* Create a table for Typica users login data */
  302. query.exec("CREATE TABLE IF NOT EXISTS typica_users(name TEXT PRIMARY KEY NOT NULL, password TEXT, active boolean NOT NULL, auto_login boolean NOT NULL)");
  303. /* Update session user logging to only use the database user when a Typica user is not explicitly provided. This maintains compatibility for mixed version use. */
  304. query.exec("CREATE OR REPLACE FUNCTION log_session_user() RETURNS trigger AS $$ BEGIN IF NEW.person IS NULL THEN NEW.person := session_user; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql");
  305. query.exec("CREATE OR REPLACE FUNCTION log_use() RETURNS trigger AS $$ DECLARE i integer := array_lower(NEW.unroasted_id, 1); u integer := array_upper(NEW.unroasted_id, 1); BEGIN IF NEW.transaction_type = 'ROAST' THEN WHILE i <= u LOOP INSERT INTO use (time, item, quantity, person) VALUES(NEW.time, NEW.unroasted_id[i], NEW.unroasted_quantity[i], NEW.person); i := i + 1; END LOOP; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql");
  306. query.exec("CREATE OR REPLACE FUNCTION log_make() RETURNS trigger AS $$ BEGIN IF NEW.roasted_quantity IS NOT NULL THEN INSERT INTO make (time, item, quantity, person) VALUES(NEW.time, NEW.roasted_id, NEW.roasted_quantity, NEW.person); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql");
  307. query.exec("CREATE TABLE IF NOT EXISTS sample_roast_profiles(\"time\" timestamp without time zone NOT NULL, profile_name TEXT NOT NULL, file bigint NOT NULL)");
  308. query.exec("UPDATE TypicaFeatures SET version = 7 WHERE feature = 'base-features'");
  309. query = query.invalidate();
  310. };
  311. /* Updates for Typica version 1.9 */
  312. var DBUpdate19 = function() {
  313. var query = new QSqlQuery;
  314. query.exec("ALTER TABLE roasting_specification ADD COLUMN spec jsonb");
  315. query.exec("UPDATE TypicaFeatures SET version = 8 WHERE feature = 'base-features'");
  316. query = query.invalidate();
  317. };
  318. if(Application.databaseConnected()) {
  319. query = new QSqlQuery();
  320. /* A table keeps track of database versioning information. This
  321. table is created
  322. if required. */
  323. query.exec("CREATE TABLE IF NOT EXISTS TypicaFeatures (feature TEXT PRIMARY KEY, enabled boolean, version bigint)");
  324. query.exec("SELECT feature, enabled, version FROM TypicaFeatures WHERE feature = 'base-features'");
  325. if(query.next())
  326. {
  327. if(query.value(2) < 1)
  328. {
  329. DBCreateBase();
  330. }
  331. if(query.value(2) < 2)
  332. {
  333. DBUpdateMultiUser();
  334. DBUpdateHistory();
  335. }
  336. if(query.value(2) < 3)
  337. {
  338. DBUpdateNotifications();
  339. }
  340. if(query.value(2) < 4)
  341. {
  342. DBUpdateTriggers();
  343. }
  344. if(query.value(2) < 5)
  345. {
  346. DBUpdateReminders();
  347. }
  348. if(query.value(2) < 6)
  349. {
  350. DBUpdateSpecification();
  351. }
  352. if(query.value(2) < 7) {
  353. DBUpdate18();
  354. }
  355. if(query.value(2) < 8) {
  356. DBUpdate19();
  357. }
  358. }
  359. else
  360. {
  361. DBCreateBase();
  362. DBUpdateMultiUser();
  363. DBUpdateHistory();
  364. DBUpdateNotifications();
  365. DBUpdateTriggers();
  366. DBUpdateReminders();
  367. DBUpdateSpecification();
  368. DBUpdate18();
  369. DBUpdate19();
  370. }
  371. query.exec("SELECT feature, enabled, version FROM TypicaFeatures WHERE feature = 'sample-roasting'");
  372. if(query.next())
  373. {
  374. if(query.value(2) < 1)
  375. {
  376. DBCreateSampleRoasting();
  377. }
  378. }
  379. else
  380. {
  381. DBCreateSampleRoasting();
  382. }
  383. var promptNewUsers = true;
  384. query.exec("SELECT count(1) FROM typica_users");
  385. if(query.next()) {
  386. if(Number(query.value(0)) > 0) {
  387. promptNewUsers = false;
  388. }
  389. }
  390. if(promptNewUsers) {
  391. var newUserDialog = new NewTypicaUser();
  392. newUserDialog.exec();
  393. }
  394. if(!Application.autoLogin()) {
  395. var loginDialog = new LoginDialog();
  396. loginDialog.exec();
  397. }
  398. query = query.invalidate();
  399. }
  400. var switchuser = findChildObject(this, 'switchuser');
  401. switchuser.triggered.connect(function() {
  402. var loginDialog = new LoginDialog();
  403. loginDialog.exec();
  404. });
  405. var createuser = findChildObject(this, 'createuser');
  406. createuser.triggered.connect(function() {
  407. var newUserDialog = new NewTypicaUser();
  408. newUserDialog.exec();
  409. });
  410. ]]>
  411. </program>
  412. </window>