Browse Source

Merge branch 'release-1.8'

Neal Wilson 7 years ago
parent
commit
33f1cae8b6
79 changed files with 18904 additions and 10306 deletions
  1. 29
    7
      README
  2. 3
    0
      config/ImportFilters/Example.js
  3. 27
    0
      config/ImportFilters/IKAWA.js
  4. 13
    13
      config/Reports/dailyproductiondetail.xml
  5. 117
    0
      config/Reports/greenforroasted.xml
  6. 50
    33
      config/Reports/invchange.xml
  7. 369
    323
      config/Reports/itemtransactions.xml
  8. 38
    0
      config/Scripts/batchtag.css
  9. 346
    0
      config/Scripts/qrcode.js
  10. BIN
      config/Translations/config.de.qm
  11. 1982
    1782
      config/Translations/config.de.ts
  12. 3008
    2307
      config/Translations/config.ts
  13. 7
    5
      config/Windows/greeninventory.xml
  14. 2
    1
      config/Windows/greensales.xml
  15. 496
    0
      config/Windows/manuallogentry.xml
  16. 125
    65
      config/Windows/navigation.xml
  17. 124
    15
      config/Windows/newbatch.xml
  18. 35
    33
      config/Windows/newsamplebatch.xml
  19. 870
    767
      config/Windows/productionroaster.xml
  20. 2
    1
      config/Windows/purchase.xml
  21. 1
    0
      config/config.xml
  22. BIN
      docs/documentation/windowreference/manageroasted.png
  23. 1
    1
      src/3rdparty/qextserialport/src/qextserialenumerator_linux.cpp
  24. 539
    265
      src/Translations/Typica.ts
  25. BIN
      src/Translations/Typica_de.qm
  26. 538
    264
      src/Translations/Typica_de.ts
  27. 5
    2
      src/Typica.pro
  28. 5
    5
      src/abouttypica.cpp
  29. 2
    2
      src/abouttypica.h
  30. 20
    20
      src/daterangeselector.cpp
  31. 4
    4
      src/daterangeselector.h
  32. 1
    1
      src/daterangeselector.w
  33. 2
    2
      src/draglabel.cpp
  34. 2
    2
      src/draglabel.h
  35. 5
    1
      src/graphsettings.w
  36. 7
    7
      src/helpmenu.cpp
  37. 2
    2
      src/helpmenu.h
  38. 10
    10
      src/licensewindow.cpp
  39. 2
    2
      src/licensewindow.h
  40. 285
    0
      src/mergeseries.w
  41. 12
    6
      src/moc_scale.cpp
  42. 2013
    819
      src/moc_typica.cpp
  43. 748
    0
      src/modbus.w
  44. 162
    0
      src/plugins.w
  45. 22
    0
      src/printerselector.cpp
  46. 19
    0
      src/printerselector.h
  47. 116
    0
      src/printerselector.w
  48. 866
    792
      src/qrc_resources.cpp
  49. 4
    4
      src/resources/Info.plist
  50. 17
    13
      src/resources/html/about.html
  51. BIN
      src/resources/icons/appicons/logo.icns
  52. BIN
      src/resources/icons/appicons/logo.ico
  53. 84
    18
      src/resources/icons/appicons/logo.svg
  54. BIN
      src/resources/icons/appicons/logo16.ico
  55. BIN
      src/resources/icons/appicons/logo16.png
  56. BIN
      src/resources/icons/appicons/logo24.png
  57. BIN
      src/resources/icons/appicons/logo256.ico
  58. BIN
      src/resources/icons/appicons/logo32.ico
  59. BIN
      src/resources/icons/appicons/logo48.ico
  60. BIN
      src/resources/icons/appicons/logo48.png
  61. BIN
      src/resources/icons/appicons/logo96.png
  62. 38
    16
      src/scale.cpp
  63. 6
    2
      src/scale.h
  64. 68
    6
      src/scales.w
  65. 220
    0
      src/thresholdannotation.w
  66. 4671
    2559
      src/typica.cpp
  67. 7
    7
      src/typica.rc
  68. 326
    25
      src/typica.w
  69. 6
    6
      src/units.cpp
  70. 2
    2
      src/units.h
  71. 0
    38
      src/unsupportedserial.w
  72. 347
    0
      src/user.w
  73. 9
    9
      src/webelement.cpp
  74. 3
    3
      src/webelement.h
  75. 32
    22
      src/webview.cpp
  76. 3
    3
      src/webview.h
  77. 26
    11
      src/webview.w
  78. 3
    3
      typica.desktop
  79. BIN
      typica.png

+ 29
- 7
README View File

@@ -1,12 +1,9 @@
1
-Typica is a free cross platform application for recording, managing, and using
2
-common records generated in commercial specialty coffee roasting operations
3
-such as roast profiles and green coffee inventory records.
4
-
5
-Project web site: http://www.randomfield.com/programs/typica/
1
+Typica is a free cross platform application for coffee roasting operations.
2
+More information is available on the project web site: https://typica.us/
6 3
 
7 4
 Typica is free software released under the MIT license as follows:
8 5
 
9
-Copyright 2007-2016 Neal Evan Wilson
6
+Copyright 2007-2017 Neal Evan Wilson
10 7
 
11 8
 Permission is hereby granteed, free of charge, to any person obtaining a copy
12 9
 of this software and associated documentation files (the "Software"), to deal
@@ -54,4 +51,29 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
54 51
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
55 52
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
56 53
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
57
-SOFTWARE.
54
+SOFTWARE.
55
+
56
+Parts of Typica are from qrcode-svg which is also used under the MIT license
57
+as follows:
58
+
59
+The MIT License (MIT)
60
+
61
+Copyright (c) 2016 papnkukn
62
+
63
+Permission is hereby granted, free of charge, to any person obtaining a copy
64
+of this software and associated documentation files (the "Software"), to deal
65
+in the Software without restriction, including without limitation the rights
66
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
67
+copies of the Software, and to permit persons to whom the Software is
68
+furnished to do so, subject to the following conditions:
69
+
70
+The above copyright notice and this permission notice shall be included in all
71
+copies or substantial portions of the Software.
72
+
73
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
74
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
75
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
76
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
77
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
78
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
79
+SOFTWARE.

+ 3
- 0
config/ImportFilters/Example.js View File

@@ -0,0 +1,3 @@
1
+print(pluginContext.text);
2
+print("A plugin has run\n");
3
+pluginContext.cq.trigger();

+ 27
- 0
config/ImportFilters/IKAWA.js View File

@@ -0,0 +1,27 @@
1
+pluginContext.table.setHeaderData(0, "Time");
2
+pluginContext.table.setHeaderData(1, "Temperature");
3
+pluginContext.table.setHeaderData(2, "Set");
4
+pluginContext.table.setHeaderData(3, "Fan");
5
+pluginContext.table.setHeaderData(4, "Heater");
6
+pluginContext.table.setHeaderData(5, "Note");
7
+pluginContext.table.clearOutputColumns();
8
+pluginContext.table.addOutputTemperatureColumn(1);
9
+pluginContext.table.addOutputTemperatureColumn(2);
10
+pluginContext.table.addOutputControlColumn(3);
11
+pluginContext.table.addOutputControlColumn(4);
12
+pluginContext.table.addOutputAnnotationColumn(5);
13
+var lines = pluginContext.data.split('\n');
14
+for(var i = 0; i < lines.length; i++) {
15
+	var fields = lines[i].split(',');
16
+	if(fields[5] == "roasting") {
17
+		var time = new QTime;
18
+		time = time.addSecs(Number(fields[0]));
19
+		pluginContext.newMeasurement(new Measurement(Units.convertTemperature(fields[4], Units.Celsius, Units.Fahrenheit), time), 1);
20
+		pluginContext.newMeasurement(new Measurement(Units.convertTemperature(fields[2], Units.Celsius, Units.Fahrenheit), time), 2);
21
+		pluginContext.newMeasurement(new Measurement(fields[1], time, Units.Unitless), 3);
22
+		pluginContext.newMeasurement(new Measurement(fields[6], time, Units.Unitless), 4);
23
+	}
24
+}
25
+for(var i = 1; i < 5; i++) {
26
+	pluginContext.table.newAnnotation("End", i, 5);
27
+}

+ 13
- 13
config/Reports/dailyproductiondetail.xml View File

@@ -307,19 +307,19 @@
307 307
 					output.writeEndElement();
308 308
 					output.writeEndElement();	
309 309
 					output.writeEndElement();
310
-                                        output.writeStartElement("td");
311
-                                        output.writeAttribute("colspan", "4");
312
-                                        if(query.value(14)) {
313
-                                            output.writeTextElement("strong", "Roast Specification Notes");
314
-                                            var specArray = query.value(14).split("\n");
315
-                                            for(var i = 0; i < noteArray.length; i++) {
316
-                                                output.writeStartElement("p");
317
-                                                output.writeAttribute("style", "margin-top: 0; margin-bottom: 0");
318
-                                                output.writeCharacters(specArray[i]);
319
-                                                output.writeEndElement();
320
-                                            }                                            
321
-                                        }
322
-                                        output.writeEndElement();
310
+                    output.writeStartElement("td");
311
+                    output.writeAttribute("colspan", "4");
312
+                    if(query.value(14)) {
313
+                        output.writeTextElement("strong", "Roast Specification Notes");
314
+                        var specArray = query.value(14).split("\n");
315
+                        for(var i = 0; i < specArray.length; i++) {
316
+                            output.writeStartElement("p");
317
+                            output.writeAttribute("style", "margin-top: 0; margin-bottom: 0");
318
+                            output.writeCharacters(specArray[i]);
319
+                            output.writeEndElement();
320
+                        }                                            
321
+                    }
322
+                    output.writeEndElement();
323 323
 					output.writeEndElement();
324 324
 				}
325 325
 				output.writeEndElement();

+ 117
- 0
config/Reports/greenforroasted.xml View File

@@ -0,0 +1,117 @@
1
+<window id="greenforroasted">
2
+    <reporttitle>Production:->Green Coffees Used for Roasted Coffees</reporttitle>
3
+    <layout type="vertical">
4
+        <layout type="horizontal">
5
+            <daterange id="dates" initial="19" /><!-- Year to Date -->
6
+            <label>Weight Unit:</label>
7
+            <sqldrop id="unit" />
8
+            <stretch />
9
+        </layout>
10
+        <webview id="report" />
11
+    </layout>
12
+    <menu name="File">
13
+        <item id="print" shortcut="Ctrl+P">Print...</item>
14
+    </menu>
15
+    <program>
16
+        <![CDATA[
17
+            this.windowTitle = TTR("greenforroasted", "Typica - Green Coffees Used for Roasted Coffees");
18
+            var report = findChildObject(this, 'report');
19
+            var printMenu = findChildObject(this, 'print');
20
+            printMenu.triggered.connect(function() {
21
+                report.print();
22
+            });
23
+            var dateSelect = findChildObject(this, 'dates');
24
+            var dateQuery = new QSqlQuery;
25
+            dateQuery.exec("SELECT time::date FROM transactions WHERE time = (SELECT min(time) FROM transactions) OR time = (SELECT max(time) FROM transactions) ORDER BY time ASC");
26
+            dateQuery.next();
27
+            var lifetimeStartDate = dateQuery.value(0);
28
+            var lifetimeEndDate;
29
+            if(dateQuery.next()) {
30
+                lifetimeEndDate = dateQuery.value(0);
31
+            } else {
32
+                lifetimeEndDate = lifetimeStartDate;
33
+            }
34
+            dateSelect.setLifetimeRange(lifetimeStartDate, lifetimeEndDate);
35
+            dateQuery = dateQuery.invalidate();
36
+            dateSelect.rangeUpdated.connect(refresh);
37
+            var unitBox = findChildObject(this, 'unit');
38
+            unitBox.addItem(TTR("greenforroasted", "Kg"));
39
+            unitBox.addItem(TTR("greenforroasted", "Lb"));
40
+            unitBox.currentIndex = QSettings.value("script/report_unit", 1);
41
+            unitBox['currentIndexChanged(int)'].connect(function() {
42
+                QSettings.setValue("script/report_unit", unitBox.currentIndex);
43
+                refresh();
44
+            });
45
+            function refresh() {
46
+                var dateRange = dateSelect.currentRange();
47
+                var startDate = dateRange[0];
48
+                var endDate = dateRange[dateRange.length - 1];
49
+                var conversion = 1;
50
+                var unitText = TTR("greensales", "Lb");
51
+                if(unitBox.currentIndex == 0) {
52
+                    conversion = 2.2;
53
+                    unitText = TTR("greensales", "Kg");
54
+                }
55
+                var buffer = new QBuffer;
56
+                buffer.open(3);
57
+                var output = new XmlWriter(buffer);
58
+                output.writeStartDocument("1.0");
59
+                output.writeDTD('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg.dtd">');
60
+                output.writeStartElement("html");
61
+                output.writeAttribute("xmlns", "http://www.w3.org/1999/xhtml");
62
+                output.writeStartElement("head");
63
+                output.writeTextElement("title", TTR("greenforroasted", "Green Coffees Used for Roasted Coffees"));
64
+                output.writeEndElement(); //head
65
+                output.writeStartElement("body");
66
+                output.writeTextElement("h1", TTR("greenforroasted", "Green Coffees Used for Roasted Coffees ") + startDate + "-" + endDate);
67
+                output.writeStartElement("ul");
68
+                var query = new QSqlQuery();
69
+                query.prepare("WITH q AS (SELECT roasted_id, unroasted_id, (SELECT name FROM items WHERE id = roasted_id) AS rname, generate_subscripts(unroasted_id, 1) AS s, sum(roasted_quantity)/:c1 AS rq, min(time)::date, max(time)::date FROM roasting_log WHERE time >= :sd1 AND time < :ed1 ::date + interval '1 day' GROUP BY roasted_id, unroasted_id) SELECT q.*, (SELECT name FROM items WHERE id = unroasted_id[q.s]) || ' (' || unroasted_id[q.s] || ')' AS gname, (SELECT SUM(unroasted_quantity[q.s])/:c2 AS gq FROM roasting_log WHERE roasted_id = q.roasted_id AND unroasted_id = q.unroasted_id AND time >= :sd2 AND time < :ed2 ::date + interval '1 day') FROM q ORDER BY q.rname ASC, min ASC");
70
+                query.bind(":sd1", startDate);
71
+                query.bind(":sd2", startDate);
72
+                query.bind(":ed1", endDate);
73
+                query.bind(":ed2", endDate);
74
+                query.bind(":c1", conversion);
75
+                query.bind(":c2", conversion);
76
+                query.exec();
77
+                var prevRid = -1;
78
+                var prevGid = "";
79
+                var first = true;
80
+                while(query.next()) {
81
+                    if(query.value(0) != prevRid) { // New roasted item
82
+                        if(!first) {
83
+                            output.writeEndElement(); //ul from previous roasted item
84
+                            output.writeEndElement(); //ul from previous roasted item
85
+                        } else {
86
+                            first = false;
87
+                        }
88
+                        output.writeTextElement("li", query.value(2) + " (" + query.value(0) + ")");
89
+                        output.writeStartElement("ul");
90
+                        output.writeTextElement("li", query.value(4) + " " + unitText + " roasted between " + query.value(5) + " and " + query.value(6) + " with:");
91
+                        output.writeStartElement("ul");
92
+                        output.writeTextElement("li", query.value(8) + " " + unitText + " of " + query.value(7));
93
+                    }
94
+                    else { // Same roasted item
95
+                        if(query.value(1) != prevGid) { // New set of greens
96
+                            output.writeEndElement(); //ul
97
+                            output.writeTextElement("li", query.value(4) + " " + unitText + " roasted between " + query.value(5) + " and " + query.value(6) + " with:");
98
+                            output.writeStartElement("ul");
99
+                            output.writeTextElement("li", query.value(8) + " " + unitText + " of " + query.value(7));
100
+                        } else { // Same set of greens
101
+                            output.writeTextElement("li", query.value(8) + " " + unitText + " of " + query.value(7));
102
+                        }
103
+                    }
104
+                    prevRid = query.value(0);
105
+                    prevGid = query.value(1);
106
+                }
107
+                output.writeEndElement(); //ul
108
+                output.writeEndElement(); //body
109
+                output.writeEndElement(); //html
110
+                output.writeEndDocument();
111
+                report.setContent(buffer);
112
+                buffer.close();
113
+            }
114
+            refresh();
115
+        ]]>
116
+    </program>
117
+</window>

+ 50
- 33
config/Reports/invchange.xml View File

@@ -66,7 +66,7 @@
66 66
 					unitText = TTR("invchange", "Kg");
67 67
 				}
68 68
 				var query = new QSqlQuery();
69
-					var q = "WITH q AS (SELECT id, name, reference, COALESCE((SELECT balance FROM item_history(id) WHERE time = (SELECT max(time) FROM item_history(id) WHERE time < :sd1)), 0)/:c1 AS starting_balance, COALESCE((SELECT sum(quantity) FROM purchase WHERE item = id AND time >= :sd2 AND time < :ed1 ::date + interval '1 day'), 0)/:c2 AS purchase, COALESCE((SELECT sum(quantity) FROM use WHERE item = id AND time >= :sd3 AND time < :ed2 ::date + interval '1 day'), 0)/:c3 AS use, COALESCE((SELECT sum(quantity) FROM sale WHERE item = id AND time >= :sd4 AND time < :ed3 ::date + interval '1 day'), 0)/:c4 AS sale, (SElECT balance FROM item_history(id) WHERE time = (SELECT max(time) FROM item_history(id) WHERE time < :ed4 ::date + interval '1 day'))/:c5 AS quantity, (SELECT sum(cost * quantity) / sum(quantity) FROM purchase WHERE item = id) AS unit_cost FROM coffees WHERE id IN (SELECT item FROM purchase WHERE time >= :sd6 AND time < :ed5 ::date + interval '1 day') OR id IN (SELECT id FROM items WHERE (SELECT balance FROM item_history(id) WHERE time = (SELECT max(time) FROM item_history(id) WHERE time < :ed6 ::date + interval '1 day')) > 0) OR id IN (SELECT DISTINCT item FROM all_transactions WHERE time > :sd7 AND time < :ed7 ::date + interval '1 day')) SELECT *, (starting_balance + purchase - use - sale - quantity) AS adjustment, starting_balance * unit_cost * :c8 AS starting_cost, purchase * unit_cost * :c9 AS purchase_cost, use * unit_cost * :c10 AS use_cost, sale * unit_cost * :c11 AS sale_cost, quantity * unit_cost * :c12 AS quantity_cost, (starting_balance + purchase - use - sale - quantity) * unit_cost * :c13 AS adjustment_cost, (SELECT sum(quantity)/:c6 FROM purchase WHERE item = id) AS total_purchase FROM q ORDER BY name";
69
+                                var q = "WITH q AS (SELECT id, name, reference, COALESCE((SELECT balance FROM item_history(id) WHERE time = (SELECT max(time) FROM item_history(id) WHERE time < :sd1)), 0)/:c1 AS starting_balance, COALESCE((SELECT sum(quantity) FROM purchase WHERE item = id AND time >= :sd2 AND time < :ed1 ::date + interval '1 day'), 0)/:c2 AS purchase, COALESCE((SELECT sum(quantity) FROM use WHERE item = id AND time >= :sd3 AND time < :ed2 ::date + interval '1 day'), 0)/:c3 AS use, COALESCE((SELECT sum(quantity) FROM sale WHERE item = id AND time >= :sd4 AND time < :ed3 ::date + interval '1 day'), 0)/:c4 AS sale, COALESCE((SELECT sum(quantity) FROM loss WHERE item = id AND time >= :sd8 AND time < :ed8 ::date + interval '1 day'), 0)/:c14 AS loss, (SElECT balance FROM item_history(id) WHERE time = (SELECT max(time) FROM item_history(id) WHERE time < :ed4 ::date + interval '1 day'))/:c5 AS quantity, (SELECT sum(cost * quantity) / sum(quantity) FROM purchase WHERE item = id) AS unit_cost FROM coffees WHERE id IN (SELECT item FROM purchase WHERE time >= :sd6 AND time < :ed5 ::date + interval '1 day') OR id IN (SELECT id FROM items WHERE (SELECT balance FROM item_history(id) WHERE time = (SELECT max(time) FROM item_history(id) WHERE time < :sd9 ::date)) <> 0) OR id IN (SELECT DISTINCT item FROM all_transactions WHERE time > :sd7 AND time < :ed7 ::date + interval '1 day')) SELECT *, (starting_balance + purchase - use - sale - loss - quantity) AS adjustment, starting_balance * unit_cost * :c8 AS starting_cost, purchase * unit_cost * :c9 AS purchase_cost, use * unit_cost * :c10 AS use_cost, sale * unit_cost * :c11 AS sale_cost, loss * unit_cost * :c15 AS loss_cost, quantity * unit_cost * :c12 AS quantity_cost, (starting_balance + purchase - use - sale - loss - quantity) * unit_cost * :c13 AS adjustment_cost, (SELECT sum(quantity)/:c6 FROM purchase WHERE item = id) AS total_purchase FROM q ORDER BY name";
70 70
 				query.prepare(q);
71 71
 				query.bind(":sd1", startDate);
72 72
 				query.bind(":sd2", startDate);
@@ -74,13 +74,15 @@
74 74
 				query.bind(":sd4", startDate);
75 75
 				query.bind(":sd6", startDate);
76 76
 				query.bind(":sd7", startDate);
77
+                                query.bind(":sd8", startDate);
78
+                                query.bind(":sd9", startDate);
77 79
 				query.bind(":ed1", endDate);
78 80
 				query.bind(":ed2", endDate);
79 81
 				query.bind(":ed3", endDate);
80 82
 				query.bind(":ed4", endDate);
81 83
 				query.bind(":ed5", endDate);
82
-				query.bind(":ed6", endDate);
83 84
 				query.bind(":ed7", endDate);
85
+                                query.bind(":ed8", endDate);
84 86
 				query.bind(":c1", conversion);
85 87
 				query.bind(":c2", conversion);
86 88
 				query.bind(":c3", conversion);
@@ -93,6 +95,8 @@
93 95
 				query.bind(":c11", conversion);
94 96
 				query.bind(":c12", conversion);
95 97
 				query.bind(":c13", conversion);
98
+                                query.bind(":c14", conversion);
99
+                                query.bind(":c15", conversion);
96 100
 				query.exec();
97 101
 				output.writeStartElement("table");
98 102
 				output.writeAttribute("rules", "groups");
@@ -103,17 +107,19 @@
103 107
 				output.writeTextElement("th", TTR("invchange", "Coffee")); // 1
104 108
 				output.writeTextElement("th", TTR("invchange", "Reference")); // 2
105 109
 				output.writeTextElement("th", TTR("invchange", "Starting (") + unitText + ")"); // 3
106
-				output.writeTextElement("th", TTR("invchange", "Cost")); // 10
107
-				output.writeTextElement("th", TTR("invchange", "Purchase (") + unitText + ")"); // 4
108 110
 				output.writeTextElement("th", TTR("invchange", "Cost")); // 11
109
-				output.writeTextElement("th", TTR("invchange", "Use (") + unitText + ")"); // 5
111
+				output.writeTextElement("th", TTR("invchange", "Purchase (") + unitText + ")"); // 4
110 112
 				output.writeTextElement("th", TTR("invchange", "Cost")); // 12
111
-				output.writeTextElement("th", TTR("invchange", "Sale (") + unitText + ")"); // 6
113
+				output.writeTextElement("th", TTR("invchange", "Use (") + unitText + ")"); // 5
112 114
 				output.writeTextElement("th", TTR("invchange", "Cost")); // 13
113
-				output.writeTextElement("th", TTR("invchange", "Adjustment (") + unitText + ")"); // 9
114
-				output.writeTextElement("th", TTR("invchange", "Cost")); // 15
115
-				output.writeTextElement("th", TTR("invchange", "Ending (") + unitText + ")"); // 7
115
+				output.writeTextElement("th", TTR("invchange", "Sale (") + unitText + ")"); // 6
116 116
 				output.writeTextElement("th", TTR("invchange", "Cost")); // 14
117
+                                output.writeTextElement("th", TTR("invchange", "Loss (") + unitText + ")"); // 7
118
+                                output.writeTextElement("th", TTR("invchange", "Cost")) // 15
119
+				output.writeTextElement("th", TTR("invchange", "Adjustment (") + unitText + ")"); // 10
120
+				output.writeTextElement("th", TTR("invchange", "Cost")); // 17
121
+				output.writeTextElement("th", TTR("invchange", "Ending (") + unitText + ")"); // 8
122
+				output.writeTextElement("th", TTR("invchange", "Cost")); // 16
117 123
 				output.writeEndElement();
118 124
 				output.writeEndElement();
119 125
 				output.writeStartElement("tbody");
@@ -128,7 +134,9 @@
128 134
 				var sum9 = 0;
129 135
 				var sum15 = 0;
130 136
 				var sum7 = 0;
131
-				var sum14 = 0;;
137
+				var sum14 = 0;
138
+                                var loss_sum = 0;
139
+                                var loss_cost_sum = 0;
132 140
 				while(query.next())
133 141
 				{
134 142
 					output.writeStartElement("tr");
@@ -141,48 +149,55 @@
141 149
 					output.writeTextElement("td", query.value(1)); //Coffee
142 150
 					output.writeTextElement("td", query.value(2)); //Reference
143 151
 					output.writeStartElement("td"); //Starting Wt
144
-					output.writeAttribute("title", (parseFloat(query.value(3))/parseFloat(query.value(16)) * 100).toFixed(0) + "%");
152
+					output.writeAttribute("title", (parseFloat(query.value(3))/parseFloat(query.value(18)) * 100).toFixed(0) + "%");
145 153
 					output.writeCDATA(parseFloat(query.value(3)).toFixed(2));
146 154
 					output.writeEndElement(); //End of Starting Wt.
147
-					output.writeTextElement("td", parseFloat(query.value(10)).toFixed(2)); //Starting Cost
155
+					output.writeTextElement("td", parseFloat(query.value(11)).toFixed(2)); //Starting Cost
148 156
 					output.writeStartElement("td"); //Purchase Wt
149
-					output.writeAttribute("title", (parseFloat(query.value(4))/parseFloat(query.value(16)) * 100).toFixed(0) + "%");
157
+					output.writeAttribute("title", (parseFloat(query.value(4))/parseFloat(query.value(18)) * 100).toFixed(0) + "%");
150 158
 					output.writeCDATA(parseFloat(query.value(4)).toFixed(2));
151 159
 					output.writeEndElement(); //End of Purchase Wt
152
-					output.writeTextElement("td", parseFloat(query.value(11)).toFixed(2)); //Purchase Cost
160
+					output.writeTextElement("td", parseFloat(query.value(12)).toFixed(2)); //Purchase Cost
153 161
 					output.writeStartElement("td"); //Use Wt
154
-					output.writeAttribute("title", (parseFloat(query.value(5))/parseFloat(query.value(16)) * 100).toFixed(0) + "%");
162
+					output.writeAttribute("title", (parseFloat(query.value(5))/parseFloat(query.value(18)) * 100).toFixed(0) + "%");
155 163
 					output.writeCDATA(parseFloat(query.value(5)).toFixed(2));
156 164
 					output.writeEndElement(); //End of Use Wt
157
-					output.writeTextElement("td", parseFloat(query.value(12)).toFixed(2)); //Use Cost
165
+					output.writeTextElement("td", parseFloat(query.value(13)).toFixed(2)); //Use Cost
158 166
 					output.writeStartElement("td"); //Sale Wt
159
-					output.writeAttribute("title", (parseFloat(query.value(6))/parseFloat(query.value(16)) * 100).toFixed(0) + "%");
167
+					output.writeAttribute("title", (parseFloat(query.value(6))/parseFloat(query.value(18)) * 100).toFixed(0) + "%");
160 168
 					output.writeCDATA(parseFloat(query.value(6)).toFixed(2));
161 169
 					output.writeEndElement(); //End of Sale Wt
162
-					output.writeTextElement("td", parseFloat(query.value(13)).toFixed(2)); //Sale Cost
163
-					output.writeStartElement("td"); //Adjustment Wt
164
-					output.writeAttribute("title", (parseFloat(query.value(9))/parseFloat(query.value(16)) * 100).toFixed(0) + "%");
165
-					output.writeCDATA(parseFloat(query.value(9)).toFixed(2));
170
+					output.writeTextElement("td", parseFloat(query.value(14)).toFixed(2)); //Sale Cost
171
+					output.writeStartElement("td"); //Loss Wt
172
+                                        output.writeAttribute("title", (parseFloat(query.value(7))/parseFloat(query.value(18)) * 100).toFixed(0) + "%");
173
+                                        output.writeCDATA(parseFloat(query.value(7)).toFixed(2));
174
+                                        output.writeEndElement(); //End of loss Wt;
175
+                                        output.writeTextElement("td", parseFloat(query.value(15)).toFixed(2)); //Loss Cost
176
+                                        output.writeStartElement("td"); //Adjustment Wt
177
+					output.writeAttribute("title", (parseFloat(query.value(10))/parseFloat(query.value(18)) * 100).toFixed(0) + "%");
178
+					output.writeCDATA(parseFloat(query.value(10)).toFixed(2));
166 179
 					output.writeEndElement(); //Adjustment Wt
167
-					output.writeTextElement("td", parseFloat(query.value(15)).toFixed(2)); //Adjustment Cost
180
+					output.writeTextElement("td", parseFloat(query.value(17)).toFixed(2)); //Adjustment Cost
168 181
 					output.writeStartElement("td"); //Ending Wt
169
-					output.writeAttribute("title", (parseFloat(query.value(7))/parseFloat(query.value(16)) * 100).toFixed(0) + "%");
170
-					output.writeCDATA(parseFloat(query.value(7)).toFixed(2));
182
+					output.writeAttribute("title", (parseFloat(query.value(8))/parseFloat(query.value(18)) * 100).toFixed(0) + "%");
183
+					output.writeCDATA(parseFloat(query.value(8)).toFixed(2));
171 184
 					output.writeEndElement(); //End of Ending Wt
172
-					output.writeTextElement("td", parseFloat(query.value(14)).toFixed(2)); //Ending Cost
185
+					output.writeTextElement("td", parseFloat(query.value(16)).toFixed(2)); //Ending Cost
173 186
 					output.writeEndElement();
174 187
 					sum3 += parseFloat(query.value(3));
175
-					sum10 += parseFloat(query.value(10));
188
+					sum10 += parseFloat(query.value(11));
176 189
 					sum4 += parseFloat(query.value(4));
177
-					sum11 += parseFloat(query.value(11));
190
+					sum11 += parseFloat(query.value(12));
178 191
 					sum5 += parseFloat(query.value(5));
179
-					sum12 += parseFloat(query.value(12));
192
+					sum12 += parseFloat(query.value(13));
180 193
 					sum6 += parseFloat(query.value(6));
181
-					sum13 += parseFloat(query.value(13));
182
-					sum9 += parseFloat(query.value(9));
183
-					sum15 += parseFloat(query.value(15));
184
-					sum7 += parseFloat(query.value(7));
185
-					sum14 += parseFloat(query.value(14));
194
+					sum13 += parseFloat(query.value(14));
195
+					sum9 += parseFloat(query.value(10));
196
+					sum15 += parseFloat(query.value(17));
197
+					sum7 += parseFloat(query.value(8));
198
+					sum14 += parseFloat(query.value(16));
199
+                                        loss_sum += parseFloat(query.value(7));
200
+                                        loss_cost_sum += parseFloat(query.value(15));
186 201
 				}
187 202
 				output.writeEndElement(); // tbody
188 203
 				output.writeStartElement("tfoot");
@@ -198,6 +213,8 @@
198 213
 				output.writeTextElement("td", sum12.toFixed(2));
199 214
 				output.writeTextElement("td", sum6.toFixed(2));
200 215
 				output.writeTextElement("td", sum13.toFixed(2));
216
+                                output.writeTextElement("td", loss_sum.toFixed(2));
217
+                                output.writeTextElement("td", loss_cost_sum.toFixed(2));
201 218
 				output.writeTextElement("td", sum9.toFixed(2));
202 219
 				output.writeTextElement("td", sum15.toFixed(2));
203 220
 				output.writeTextElement("td", sum7.toFixed(2));

+ 369
- 323
config/Reports/itemtransactions.xml View File

@@ -1,325 +1,371 @@
1 1
 <window id="item_transactions">
2
-	<reporttitle>Inventory:->Item Transactions</reporttitle>
3
-	<layout type="vertical">
4
-		<layout type="horizontal">
5
-			<label>Item:</label>
6
-			<sqldrop id="item" data="0" display="1" showdata="true">
7
-				<null />
8
-				<query>SELECT id, name FROM items WHERE category = 'Coffee: Unroasted' ORDER BY name</query>
9
-			</sqldrop>
10
-			<label>Weight Unit:</label>
11
-			<sqldrop id="unit" />
12
-			<stretch />
13
-		</layout>
14
-		<webview id="report" />
15
-	</layout>
16
-	<menu name="File">
17
-		<item id="print" shortcut="Ctrl+P">Print</item>
18
-	</menu>
19
-	<program>
20
-		<![CDATA[
21
-			this.windowTitle = TTR("item_transactions", "Typica - Item Transactions");
22
-			var itemBox = findChildObject(this, 'item');
23
-			var unitBox = findChildObject(this, 'unit');
24
-			unitBox.addItem(TTR("item_transactions", "Kg"));
25
-			unitBox.addItem(TTR("item_transactions", "Lb"));
26
-			unitBox.currentIndex = QSettings.value("script/report_unit", 1);
27
-			unitBox['currentIndexChanged(int)'].connect(function() {
28
-				QSettings.setValue("script/report_unit", unitBox.currentIndex);
29
-				refresh();
30
-			});
31
-			var view = findChildObject(this, 'report');
32
-			var printMenu = findChildObject(this, 'print');
33
-			printMenu.triggered.connect(function() {
34
-				view.print();
35
-			});
36
-			itemBox['currentIndexChanged(int)'].connect(function() {
37
-				refresh();
38
-			});
39
-			function refresh() {
40
-				var buffer = new QBuffer;
41
-				buffer.open(3);
42
-				var output = new XmlWriter(buffer);
43
-				output.writeStartDocument("1.0");
44
-				output.writeDTD('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg.dtd">');
45
-				output.writeStartElement("html");
46
-				output.writeAttribute("xmlns", "http://www.w3.org/1999/xhtml");
47
-				output.writeStartElement("head");
48
-				output.writeTextElement("title", TTR("item_transactions", "Item Transactions"));
49
-				output.writeStartElement("script");
50
-				var scriptFile = new QFile(QSettings.value("config") + "/Scripts/d3.min.js");
51
-				scriptFile.open(1);
52
-				output.writeCDATA(scriptFile.readToString());
53
-				scriptFile.close();
54
-				output.writeEndElement();
55
-				output.writeStartElement("style");
56
-				output.writeAttribute("type", "text/css");
57
-				output.writeCDATA("tr.PURCHASE {background-color: #77FF77}");
58
-				output.writeCDATA("tr.USE {background-color: #FFFFFF}");
59
-				output.writeCDATA("tr.INVENTORY {background-color: #7777FF}");
60
-				output.writeCDATA("tr.SALE {background-color: #FF77FF}");
61
-				output.writeCDATA("tr.LOSS {background-color: #FF7777}");
62
-				output.writeCDATA("tr.MAKE {background-color: #FFFF77}");
63
-				output.writeEndElement(); // style
64
-				
65
-				output.writeEndElement();
66
-				output.writeStartElement("body");
67
-				output.writeTextElement("h1", TTR("item_transactions", "Item Transactions:"));
68
-				output.writeStartElement("table");
69
-				output.writeStartElement("tr");
70
-				output.writeStartElement("td");
71
-				output.writeTextElement("strong", "Item: ")
72
-				output.writeTextElement("span", itemBox.currentText);
73
-				output.writeEndElement(); // td
74
-				var query = new QSqlQuery();
75
-				query.prepare("SELECT reference, category FROM items WHERE id = :item");
76
-				query.bind(":item", itemBox.currentData());
77
-				query.exec();
78
-				if(query.next()) {
79
-					output.writeStartElement("td");
80
-					output.writeTextElement("strong", TTR("item_transactions", "Reference: "));
81
-					output.writeTextElement("span", query.value(0));
82
-					output.writeEndElement(); // td
83
-					output.writeStartElement("td");
84
-					output.writeTextElement("strong", TTR("item_transactions", "Category: "));
85
-					output.writeTextElement("span", query.value(1));
86
-					output.writeEndElement(); //td
87
-					output.writeEndElement(); //tr
88
-					query.prepare("SELECT origin, region, producer, grade, milling, drying FROM coffees WHERE id = :item");
89
-					query.bind(":item", itemBox.currentData());
90
-					query.exec();
91
-					if(query.next()) {
92
-						output.writeStartElement("tr");
93
-						output.writeStartElement("td");
94
-						output.writeTextElement("strong", TTR("item_transactions", "Origin: "));
95
-						output.writeTextElement("span", query.value(0));
96
-						output.writeEndElement(); // td
97
-						output.writeStartElement("td");
98
-						output.writeTextElement("strong", TTR("item_transactions", "Region: "));
99
-						output.writeTextElement("span", query.value(1));
100
-						output.writeEndElement(); // td
101
-						output.writeStartElement("td");
102
-						output.writeTextElement("strong", TTR("item_transactions", "Producer: "));
103
-						output.writeTextElement("span", query.value(2));
104
-						output.writeEndElement(); // td
105
-						output.writeEndElement(); // tr
106
-						output.writeStartElement("tr");
107
-						output.writeStartElement("td");
108
-						output.writeTextElement("strong", TTR("item_transactions", "Grade: "));
109
-						output.writeTextElement("span", query.value(3));
110
-						output.writeEndElement(); // td
111
-						output.writeStartElement("td");
112
-						output.writeTextElement("strong", TTR("item_transactions", "Milling: "));
113
-						output.writeTextElement("span", query.value(4));
114
-						output.writeEndElement(); // td
115
-						output.writeStartElement("td");
116
-						output.writeTextElement("strong", TTR("item_transactions", "Drying: "));
117
-						output.writeTextElement("span", query.value(5));
118
-						output.writeEndElement(); // td
119
-						output.writeEndElement(); // tr
120
-						query.prepare("SELECT decaf_method FROM decaf_coffees WHERE id = :item");
121
-						query.bind(":item", itemBox.currentData());
122
-						query.exec();
123
-						if(query.next()) {
124
-							output.writeStartElement("tr");
125
-							output.writeStartElement("td");
126
-							output.writeAttribute("colspan", "3");
127
-							output.writeTextElement("strong", TTR("item_transactions", "Decaffeination Method: "));
128
-							output.writeTextElement("span", query.value(0));
129
-							output.writeEndElement(); // td
130
-							output.writeEndElement(); // tr
131
-						}
132
-					}
133
-					output.writeEndElement() // table
134
-					
135
-					output.writeStartElement("div");
136
-					output.writeAttribute("id", "chart");
137
-					output.writeEndElement();
138
-					
139
-					query.prepare("WITH q AS (SELECT roasted_id, unroasted_id, unroasted_quantity, unroasted_total_quantity, roasted_quantity, generate_subscripts(unroasted_quantity, 1) AS s FROM roasting_log) SELECT (SELECT name FROM items WHERE id = roasted_id) AS name, roasted_id, SUM(unroasted_quantity[s]) AS total, COUNT(unroasted_quantity[s]), SUM((unroasted_quantity[s]/unroasted_total_quantity)*roasted_quantity)::numeric(12,3) AS roast_proportion FROM q WHERE unroasted_id[s] = :item1 GROUP BY roasted_id UNION SELECT 'Green Sales', NULL, SUM(quantity), COUNT(1), NULL FROM sale WHERE item = :item2 UNION SELECT 'Inventory Adjustment', NULL, ((SELECT SUM(quantity) FROM purchase WHERE item = :item3) - (SELECT quantity FROM items WHERE id = :item4) - (SELECT SUM(quantity) FROM all_transactions WHERE type != 'PURCHASE' AND type != 'INVENTORY' AND item = :item5)), (SELECT COUNT(1) FROM inventory WHERE item = :item6), NULL UNION SELECT 'Loss', NULL, SUM(quantity), COUNT(1), NULL FROM loss WHERE item = :item7 UNION SELECT 'Current Inventory', NULL, (SELECT quantity FROM items WHERE id = :item8), NULL, NULL ORDER BY total DESC");
140
-					query.bind(":item1", itemBox.currentData());
141
-					query.bind(":item2", itemBox.currentData());
142
-					query.bind(":item3", itemBox.currentData());
143
-					query.bind(":item4", itemBox.currentData());
144
-					query.bind(":item5", itemBox.currentData());
145
-					query.bind(":item6", itemBox.currentData());
146
-					query.bind(":item7", itemBox.currentData());
147
-					query.bind(":item8", itemBox.currentData());
148
-					query.exec();
149
-					var chartData = "var data = [";
150
-					var roastedCoffeeLines = "";
151
-					var adjustmentLines = "";
152
-					var currentInventoryLine = "";
153
-					var conversion = 1;
154
-					if(unitBox.currentIndex == 0) {
155
-						conversion = 2.2;
156
-					}
157
-					while(query.next()) {
158
-						if(Number(query.value(1)) > 0) {
159
-							roastedCoffeeLines += "['" + query.value(0).replace(/\'/g, "\\x27") + "'," + query.value(2) / conversion + "," + query.value(3) + "," + query.value(4) / conversion + "],";
160
-						} else if (query.value(0) == "Current Inventory") {
161
-							currentInventoryLine = "['Current Inventory'," + query.value(2) / conversion + "," + query.value(3) + "," + query.value(4) / conversion + "]";
162
-						} else {
163
-							if(Number(query.value(3)) > 0) {
164
-								adjustmentLines += "['" + query.value(0) + "'," + query.value(2) / conversion + "," + query.value(3) + "," + query.value(4) / conversion + "],";
165
-							}
166
-						}
167
-					}
168
-					chartData = chartData + roastedCoffeeLines + adjustmentLines + currentInventoryLine + "];";
169
-					
170
-					output.writeTextElement("script", chartData);
171
-					
172
-					output.writeStartElement("script");
173
-					scriptFile = new QFile(QSettings.value("config") + "/Scripts/greenusechart.js");
174
-					scriptFile.open(1);
175
-					output.writeCDATA(scriptFile.readToString());
176
-					scriptFile.close();
177
-					output.writeEndElement();
178
-					
179
-					query.prepare("SELECT time::date, type, quantity / :c1, balance / :c2, (SELECT files FROM roasting_log WHERE roasting_log.time = item_history.time AND item = ANY(unroasted_id)), (SELECT invoice_id FROM invoice_items WHERE item = item_id AND item_history.type = 'PURCHASE'), (SELECT vendor || ' ' || invoice FROM invoices WHERE id = (SELECT invoice_id FROM invoice_items WHERE item = item_id AND item_history.type = 'PURCHASE')), (SELECT name FROM items WHERE id = (SELECT roasted_id FROM roasting_log WHERE roasting_log.time = item_history.time AND item = ANY(unroasted_id))), customer, reason FROM item_history(:item)");
180
-					switch(unitBox.currentIndex)
181
-					{
182
-						case 0:
183
-							query.bind(":c1", 2.2);
184
-							query.bind(":c2", 2.2);
185
-							break;
186
-						case 1:
187
-							query.bind(":c1", 1);
188
-							query.bind(":c2", 1);
189
-							break;
190
-					}
191
-					query.bind(":item", itemBox.currentData());
192
-					query.exec();
193
-					output.writeStartElement("table");
194
-					output.writeStartElement("tr");
195
-					output.writeTextElement("th", TTR("item_transactions", "Date"));
196
-					output.writeTextElement("th", TTR("item_transactions", "Type"));
197
-					output.writeTextElement("th", TTR("item_transactions", "Quantity"));
198
-					output.writeTextElement("th", TTR("item_transactions", "Balance"));
199
-					output.writeTextElement("th", TTR("item_transactions", "Record"));
200
-					output.writeEndElement(); // tr
201
-					var prev_balance = "0";
202
-					var prev_prec = 0;
203
-					var cur_prec = 0;
204
-                                        var max_prec = 3;
205
-					while(query.next()) {
206
-						output.writeStartElement("tr");
207
-						output.writeAttribute("class", query.value(1));
208
-						output.writeTextElement("td", query.value(0));
209
-						output.writeTextElement("td", query.value(1));
210
-                                                var split = prev_balance.split('.');
211
-                                                if(split.length > 1) {
212
-                                                    prev_prec = split[1].length;
213
-                                                } else {
214
-                                                    prev_prec = 0;
215
-                                                }
216
-                                                split = query.value(2).split('.');
217
-                                                if(split.length > 1) {
218
-                                                    cur_prec = split[1].length;
219
-                                                } else {
220
-                                                    cur_prec = 0;
221
-                                                }
222
-                                                var prec = prev_prec > cur_prec ? prev_prec : cur_prec;
223
-                                                var prec = (prec > max_prec ? max_prec : prec);
224
-						if(query.value(1) == "INVENTORY") {
225
-                                                    output.writeTextElement("td", (Number(query.value(2)) - Number(prev_balance)).toFixed(prec));
226
-						} else {
227
-                                                    output.writeTextElement("td", (Number(query.value(2)).toFixed(prec)));
228
-						}
229
-						output.writeTextElement("td", (Number(query.value(3)).toFixed(prec)));
230
-						prev_balance = query.value(3);
231
-						if(query.value(1) == "PURCHASE") {
232
-							output.writeStartElement("td");
233
-							output.writeStartElement("a");
234
-							output.writeAttribute("href", "typica://script/i" + query.value(5));
235
-							output.writeCDATA(query.value(6) + " (" + query.value(5) + ")");
236
-							output.writeEndElement();
237
-							output.writeEndElement();
238
-						} else if(query.value(1) == "USE") {
239
-							output.writeStartElement("td");
240
-							output.writeStartElement("a");
241
-							output.writeAttribute("href", "typica://script/p" + query.value(4).slice(1,-1));
242
-							output.writeCDATA(query.value(7) + " " + query.value(4));
243
-							output.writeEndElement();
244
-							output.writeEndElement();
245
-                                                } else if(query.value(1) == "LOSS") {
246
-                                                    output.writeTextElement("td", query.value(9));
247
-                                                } else if(query.value(1) == "SALE") {
248
-                                                    output.writeTextElement("td", query.value(8));
249
-						} else {
250
-							output.writeTextElement("td", "");
251
-						}
252
-						output.writeEndElement(); // tr
253
-					}
254
-					output.writeEndElement(); // table
255
-					/* Put the rest of the report here. No sense running queries if
256
-					   the item doesn't exist. */
257
-				} else {
258
-					/* Close tags if item data not found. */
259
-					output.writeEndElement(); // tr
260
-					output.writeEndElement(); // table
261
-				}
262
-				
263
-				output.writeEndElement(); // body
264
-				output.writeEndElement(); // html
265
-				output.writeEndDocument();
266
-				view.setContent(buffer);
267
-				buffer.close();
268
-				query = query.invalidate();
269
-			}
270
-			if(itemBox.currentData() > 0) {
271
-				refresh();
272
-			}
273
-			
274
-			/* Open invoices */
275
-			var openInvoice = function(url) {
276
-				var arg = url.slice(1, url.length);
277
-				var info = createWindow("invoiceinfo");
278
-				info.setInvoiceID(arg);
279
-				var query = new QSqlQuery();
280
-				query.exec("SELECT time, invoice, vendor FROM invoices WHERE id = " + arg);
281
-				query.next();
282
-				var timefield = findChildObject(info, 'date');
283
-				timefield.text = query.value(0);
284
-				var vendorfield = findChildObject(info, 'vendor');
285
-				vendorfield.text = query.value(2);
286
-				var invoicefield = findChildObject(info, 'invoice');
287
-				invoicefield.text = query.value(1);
288
-				var itemtable = findChildObject(info, 'itemtable');
289
-				itemtable.setQuery("SELECT record_type, item_id, description, (SELECT reference FROM items WHERE id = item_id) AS reference, (SELECT cost FROM purchase WHERE item = item_id) AS unit_cost, (SELECT quantity FROM purchase WHERE item = item_id) AS quantity, ((SELECT quantity FROM purchase WHERE item = item_id)/(SELECT conversion FROM lb_bag_conversion WHERE item = item_id))::numeric(12,2) AS sacks, cost FROM invoice_items WHERE invoice_id = " + arg + " AND record_type = 'PURCHASE' UNION SELECT record_type, NULL, description, NULL, NULL, NULL, NULL, cost FROM invoice_items WHERE invoice_id = " + arg + " AND record_type = 'FEE' ORDER BY item_id");
290
-				query = query.invalidate();
291
-			};
292
-			
293
-			/* Open batch data */
294
-			var openProfile = function(url) {
295
-				var arg = url.slice(1, url.length);
296
-				var details = createWindow("batchDetails");
297
-				var fakeTable = new Object;
298
-				fakeTable.holding = new Array(7);
299
-				fakeTable.data = function(r, c) {
300
-					return this.holding[c];
301
-				};
302
-				var query = new QSqlQuery();
303
-				query.exec("SELECT time, machine, (SELECT name FROM items WHERE id = roasted_id) AS name, unroasted_total_quantity AS green, roasted_quantity AS roasted, ((unroasted_total_quantity - roasted_quantity) / unroasted_total_quantity * 100::numeric)::numeric(12,2) AS weight_loss, duration, annotation FROM roasting_log WHERE files = '{" + arg + "}'");
304
-				query.next();
305
-				for(var i = 0; i < 8; i++) {
306
-					fakeTable.holding[i] = query.value(i);
307
-				}
308
-				query = query.invalidate();
309
-				details.loadData(fakeTable, 0);
310
-			};
311
-			
312
-			view.scriptLinkClicked.connect(function(url) {
313
-				var linkType = url[0];
314
-				switch(linkType) {
315
-					case 'i':
316
-						openInvoice(url);
317
-						break;
318
-					case 'p':
319
-						openProfile(url);
320
-						break;
321
-				}
322
-			});
323
-		]]>
324
-	</program>
2
+    <reporttitle>Inventory:->Item Transactions</reporttitle>
3
+    <layout type="vertical">
4
+        <layout type="horizontal">
5
+            <daterange id="dates" initial="23" /><!-- Lifetime-->
6
+            <label>Item:</label>
7
+            <sqldrop id="item" data="0" display="1" showdata="true">
8
+            <null />
9
+            <query>SELECT id, name FROM items WHERE category = 'Coffee: Unroasted' ORDER BY name</query>
10
+            </sqldrop>
11
+            <label>Weight Unit:</label>
12
+            <sqldrop id="unit" />
13
+            <stretch />
14
+        </layout>
15
+        <webview id="report" />
16
+    </layout>
17
+    <menu name="File">
18
+        <item id="print" shortcut="Ctrl+P">Print</item>
19
+    </menu>
20
+    <program>
21
+        <![CDATA[
22
+            this.windowTitle = TTR("item_transactions", "Typica - Item Transactions");
23
+            var dateSelect = findChildObject(this, 'dates');
24
+            var dateQuery = new QSqlQuery();
25
+            dateQuery.exec("SELECT time::date FROM transactions WHERE time = (SELECT min(time) FROM transactions) OR time = (SELECT max(time) FROM transactions) ORDER BY time ASC");
26
+            dateQuery.next();
27
+            var lifetimeStartDate = dateQuery.value(0);
28
+            var lifetimeEndDate;
29
+            if(dateQuery.next()) {
30
+                lifetimeEndDate = dateQuery.value(0);
31
+            } else {
32
+                lifetimeEndDate = lifetimeStartDate;
33
+            }
34
+            dateSelect.setLifetimeRange(lifetimeStartDate, lifetimeEndDate);
35
+            dateQuery = dateQuery.invalidate();
36
+            dateSelect.rangeUpdated.connect(function() {
37
+                refresh();
38
+            });
39
+            var itemBox = findChildObject(this, 'item');
40
+            var unitBox = findChildObject(this, 'unit');
41
+            unitBox.addItem(TTR("item_transactions", "Kg"));
42
+            unitBox.addItem(TTR("item_transactions", "Lb"));
43
+            unitBox.currentIndex = QSettings.value("script/report_unit", 1);
44
+            unitBox['currentIndexChanged(int)'].connect(function() {
45
+                QSettings.setValue("script/report_unit", unitBox.currentIndex);
46
+                refresh();
47
+            });
48
+            var view = findChildObject(this, 'report');
49
+            var printMenu = findChildObject(this, 'print');
50
+            printMenu.triggered.connect(function() {
51
+                view.print();
52
+            });
53
+            itemBox['currentIndexChanged(int)'].connect(function() {
54
+                refresh();
55
+            });
56
+            function refresh() {
57
+                var dateRange = dateSelect.currentRange();
58
+                var startDate = dateRange[0];
59
+                var endDate = dateRange[dateRange.length - 1];
60
+                var buffer = new QBuffer;
61
+                buffer.open(3);
62
+                var output = new XmlWriter(buffer);
63
+                output.writeStartDocument("1.0");
64
+                output.writeDTD('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg.dtd">');
65
+                output.writeStartElement("html");
66
+                output.writeAttribute("xmlns", "http://www.w3.org/1999/xhtml");
67
+                output.writeStartElement("head");
68
+                output.writeTextElement("title", TTR("item_transactions", "Item Transactions"));
69
+                output.writeStartElement("script");
70
+                var scriptFile = new QFile(QSettings.value("config") + "/Scripts/d3.min.js");
71
+                scriptFile.open(1);
72
+                output.writeCDATA(scriptFile.readToString());
73
+                scriptFile.close();
74
+                output.writeEndElement();
75
+                output.writeStartElement("style");
76
+                output.writeAttribute("type", "text/css");
77
+                output.writeCDATA("tr.PURCHASE {background-color: #77FF77}");
78
+                output.writeCDATA("tr.USE {background-color: #FFFFFF}");
79
+                output.writeCDATA("tr.INVENTORY {background-color: #7777FF}");
80
+                output.writeCDATA("tr.SALE {background-color: #FF77FF}");
81
+                output.writeCDATA("tr.LOSS {background-color: #FF7777}");
82
+                output.writeCDATA("tr.MAKE {background-color: #FFFF77}");
83
+                output.writeEndElement(); // style
84
+                output.writeEndElement();
85
+                output.writeStartElement("body");
86
+                output.writeTextElement("h1", TTR("item_transactions", "Item Transactions:"));
87
+                output.writeStartElement("table");
88
+                output.writeStartElement("tr");
89
+                output.writeStartElement("td");
90
+                output.writeTextElement("strong", "Item: ")
91
+                output.writeTextElement("span", itemBox.currentText);
92
+                output.writeEndElement(); // td
93
+                var query = new QSqlQuery();
94
+                query.prepare("SELECT reference, category FROM items WHERE id = :item");
95
+                query.bind(":item", itemBox.currentData());
96
+                query.exec();
97
+                if(query.next()) {
98
+                    output.writeStartElement("td");
99
+                    output.writeTextElement("strong", TTR("item_transactions", "Reference: "));
100
+                    output.writeTextElement("span", query.value(0));
101
+                    output.writeEndElement(); // td
102
+                    output.writeStartElement("td");
103
+                    output.writeTextElement("strong", TTR("item_transactions", "Category: "));
104
+                    output.writeTextElement("span", query.value(1));
105
+                    output.writeEndElement(); //td
106
+                    output.writeEndElement(); //tr
107
+                    query.prepare("SELECT origin, region, producer, grade, milling, drying FROM coffees WHERE id = :item");
108
+                    query.bind(":item", itemBox.currentData());
109
+                    query.exec();
110
+                    if(query.next()) {
111
+                        output.writeStartElement("tr");
112
+                        output.writeStartElement("td");
113
+                        output.writeTextElement("strong", TTR("item_transactions", "Origin: "));
114
+                        output.writeTextElement("span", query.value(0));
115
+                        output.writeEndElement(); // td
116
+                        output.writeStartElement("td");
117
+                        output.writeTextElement("strong", TTR("item_transactions", "Region: "));
118
+                        output.writeTextElement("span", query.value(1));
119
+                        output.writeEndElement(); // td
120
+                        output.writeStartElement("td");
121
+                        output.writeTextElement("strong", TTR("item_transactions", "Producer: "));
122
+                        output.writeTextElement("span", query.value(2));
123
+                        output.writeEndElement(); // td
124
+                        output.writeEndElement(); // tr
125
+                        output.writeStartElement("tr");
126
+                        output.writeStartElement("td");
127
+                        output.writeTextElement("strong", TTR("item_transactions", "Grade: "));
128
+                        output.writeTextElement("span", query.value(3));
129
+                        output.writeEndElement(); // td
130
+                        output.writeStartElement("td");
131
+                        output.writeTextElement("strong", TTR("item_transactions", "Milling: "));
132
+                        output.writeTextElement("span", query.value(4));
133
+                        output.writeEndElement(); // td
134
+                        output.writeStartElement("td");
135
+                        output.writeTextElement("strong", TTR("item_transactions", "Drying: "));
136
+                        output.writeTextElement("span", query.value(5));
137
+                        output.writeEndElement(); // td
138
+                        output.writeEndElement(); // tr
139
+                        query.prepare("SELECT decaf_method FROM decaf_coffees WHERE id = :item");
140
+                        query.bind(":item", itemBox.currentData());
141
+                        query.exec();
142
+                        if(query.next()) {
143
+                            output.writeStartElement("tr");
144
+                            output.writeStartElement("td");
145
+                            output.writeAttribute("colspan", "3");
146
+                            output.writeTextElement("strong", TTR("item_transactions", "Decaffeination Method: "));
147
+                            output.writeTextElement("span", query.value(0));
148
+                            output.writeEndElement(); // td
149
+                            output.writeEndElement(); // tr
150
+                        }
151
+                    }
152
+                    output.writeEndElement() // table
153
+                    output.writeStartElement("div");
154
+                    output.writeAttribute("id", "chart");
155
+                    output.writeEndElement();
156
+                    query.prepare("WITH q AS (SELECT roasted_id, unroasted_id, unroasted_quantity, unroasted_total_quantity, roasted_quantity, generate_subscripts(unroasted_quantity, 1) AS s FROM roasting_log WHERE time >= :sd AND time < :ed ::date + interval '1 day') SELECT (SELECT name FROM items WHERE id = roasted_id) AS name, roasted_id, SUM(unroasted_quantity[s]) AS total, COUNT(unroasted_quantity[s]), SUM((unroasted_quantity[s]/unroasted_total_quantity)*roasted_quantity)::numeric(12,3) AS roast_proportion FROM q WHERE unroasted_id[s] = :item1 GROUP BY roasted_id UNION SELECT 'Green Sales', NULL, SUM(quantity), COUNT(1), NULL FROM sale WHERE item = :item2 UNION SELECT 'Inventory Adjustment', NULL, ((SELECT SUM(quantity) FROM purchase WHERE item = :item3) - (SELECT quantity FROM items WHERE id = :item4) - (SELECT SUM(quantity) FROM all_transactions WHERE type != 'PURCHASE' AND type != 'INVENTORY' AND item = :item5)), (SELECT COUNT(1) FROM inventory WHERE item = :item6), NULL UNION SELECT 'Loss', NULL, SUM(quantity), COUNT(1), NULL FROM loss WHERE item = :item7 UNION SELECT 'Current Inventory', NULL, (SELECT quantity FROM items WHERE id = :item8), NULL, NULL ORDER BY total DESC");
157
+                    query.bind(":sd", startDate);
158
+                    query.bind(":ed", endDate);
159
+                    query.bind(":item1", itemBox.currentData());
160
+                    query.bind(":item2", itemBox.currentData());
161
+                    query.bind(":item3", itemBox.currentData());
162
+                    query.bind(":item4", itemBox.currentData());
163
+                    query.bind(":item5", itemBox.currentData());
164
+                    query.bind(":item6", itemBox.currentData());
165
+                    query.bind(":item7", itemBox.currentData());
166
+                    query.bind(":item8", itemBox.currentData());
167
+                    query.exec();
168
+                    var chartData = "var data = [";
169
+                    var roastedCoffeeLines = "";
170
+                    var adjustmentLines = "";
171
+                    var currentInventoryLine = "";
172
+                    var conversion = 1;
173
+                    if(unitBox.currentIndex == 0) {
174
+                        conversion = 2.2;
175
+                    }
176
+                    while(query.next()) {
177
+                        if(Number(query.value(1)) > 0) {
178
+                            roastedCoffeeLines += "['" + query.value(0).replace(/\'/g, "\\x27") + "'," + query.value(2) / conversion + "," + query.value(3) + "," + query.value(4) / conversion + "],";
179
+                        } else if (query.value(0) == "Current Inventory") {
180
+                            currentInventoryLine = "['Current Inventory'," + query.value(2) / conversion + "," + query.value(3) + "," + query.value(4) / conversion + "]";
181
+                        } else {
182
+                            if(Number(query.value(3)) > 0) {
183
+                                adjustmentLines += "['" + query.value(0) + "'," + query.value(2) / conversion + "," + query.value(3) + "," + query.value(4) / conversion + "],";
184
+                            }
185
+                        }
186
+                    }
187
+                    chartData = chartData + roastedCoffeeLines + adjustmentLines + currentInventoryLine + "];";
188
+                    output.writeTextElement("script", chartData);
189
+                    output.writeStartElement("script");
190
+                    scriptFile = new QFile(QSettings.value("config") + "/Scripts/greenusechart.js");
191
+                    scriptFile.open(1);
192
+                    output.writeCDATA(scriptFile.readToString());
193
+                    scriptFile.close();
194
+                    output.writeEndElement();
195
+                    eval(chartData);
196
+                    output.writeStartElement("table");
197
+                    output.writeStartElement("tr");
198
+                    output.writeTextElement("th", "Item");
199
+                    output.writeTextElement("th", "Green");
200
+                    output.writeTextElement("th", "Roasted");
201
+                    output.writeTextElement("th", "Transactions");
202
+                    output.writeEndElement();
203
+                    for(var r = 0; r < data.length; r++)
204
+                    {
205
+                        output.writeStartElement("tr");
206
+                        output.writeTextElement("td", data[r][0]);
207
+                        output.writeTextElement("td", data[r][1]);
208
+                        output.writeTextElement("td", data[r][3]);
209
+                        output.writeTextElement("td", data[r][2]);
210
+                        output.writeEndElement();
211
+                    }
212
+                    output.writeStartElement("tr");
213
+                    output.writeTextElement("th", "Totals:");
214
+                    output.writeTextElement("td", data.reduce(function(prev, current){
215
+                        return +(current[1]) + prev;
216
+                    }, 0));
217
+                    output.writeTextElement("td", data.reduce(function(prev, current){
218
+                        return +(current[3]) + prev;
219
+                    }, 0));
220
+                    output.writeTextElement("td", data.reduce(function(prev, current){
221
+                        return +(current[2]) + prev;
222
+                    }, 0));
223
+                    output.writeEndElement();
224
+                    output.writeEndElement();
225
+                    query.prepare("SELECT time::date, type, quantity / :c1, balance / :c2, (SELECT files FROM roasting_log WHERE roasting_log.time = item_history.time AND item = ANY(unroasted_id)), (SELECT invoice_id FROM invoice_items WHERE item = item_id AND item_history.type = 'PURCHASE'), (SELECT vendor || ' ' || invoice FROM invoices WHERE id = (SELECT invoice_id FROM invoice_items WHERE item = item_id AND item_history.type = 'PURCHASE')), (SELECT name FROM items WHERE id = (SELECT roasted_id FROM roasting_log WHERE roasting_log.time = item_history.time AND item = ANY(unroasted_id))), customer, reason, (SELECT person FROM transactions WHERE time = item_history.time AND item = item_history.item) FROM item_history(:item) WHERE time >= :sd AND time < :ed ::date + interval '1 day'");
226
+                    query.bind(":sd", startDate);
227
+                    query.bind(":ed", endDate);
228
+                    switch(unitBox.currentIndex)
229
+                    {
230
+                        case 0:
231
+                            query.bind(":c1", 2.2);
232
+                            query.bind(":c2", 2.2);
233
+                            break;
234
+                        case 1:
235
+                            query.bind(":c1", 1);
236
+                            query.bind(":c2", 1);
237
+                            break;
238
+                    }
239
+                    query.bind(":item", itemBox.currentData());
240
+                    query.exec();
241
+                    output.writeStartElement("table");
242
+                    output.writeStartElement("tr");
243
+                    output.writeTextElement("th", TTR("item_transactions", "Date"));
244
+                    output.writeTextElement("th", TTR("item_transactions", "Type"));
245
+                    output.writeTextElement("th", TTR("item_transactions", "Quantity"));
246
+                    output.writeTextElement("th", TTR("item_transactions", "Balance"));
247
+                    output.writeTextElement("th", TTR("item_transactions", "Record"));
248
+					output.writeTextElement("th", TTR("item_transactions", "Person"));
249
+                    output.writeEndElement(); // tr
250
+                    var prev_balance = "0";
251
+                    var prev_prec = 0;
252
+                    var cur_prec = 0;
253
+                    var max_prec = 3;
254
+                    while(query.next()) {
255
+                        output.writeStartElement("tr");
256
+                        output.writeAttribute("class", query.value(1));
257
+                        output.writeTextElement("td", query.value(0));
258
+                        output.writeTextElement("td", query.value(1));
259
+                        var split = prev_balance.split('.');
260
+                        if(split.length > 1) {
261
+                            prev_prec = split[1].length;
262
+                        } else {
263
+                            prev_prec = 0;
264
+                        }
265
+                        split = query.value(2).split('.');
266
+                        if(split.length > 1) {
267
+                            cur_prec = split[1].length;
268
+                        } else {
269
+                            cur_prec = 0;
270
+                        }
271
+                        var prec = prev_prec > cur_prec ? prev_prec : cur_prec;
272
+                        var prec = (prec > max_prec ? max_prec : prec);
273
+                        if(query.value(1) == "INVENTORY") {
274
+                            output.writeTextElement("td", (Number(query.value(2)) - Number(prev_balance)).toFixed(prec));
275
+                        } else {
276
+                            output.writeTextElement("td", (Number(query.value(2)).toFixed(prec)));
277
+                        }
278
+                        output.writeTextElement("td", (Number(query.value(3)).toFixed(prec)));
279
+                                prev_balance = query.value(3);
280
+                        if(query.value(1) == "PURCHASE") {
281
+                            output.writeStartElement("td");
282
+                            output.writeStartElement("a");
283
+                            output.writeAttribute("href", "typica://script/i" + query.value(5));
284
+                            output.writeCDATA(query.value(6) + " (" + query.value(5) + ")");
285
+                            output.writeEndElement();
286
+                            output.writeEndElement();
287
+                        } else if(query.value(1) == "USE") {
288
+                            output.writeStartElement("td");
289
+                            output.writeStartElement("a");
290
+                            output.writeAttribute("href", "typica://script/p" + query.value(4).slice(1,-1));
291
+                            output.writeCDATA(query.value(7) + " " + query.value(4));
292
+                            output.writeEndElement();
293
+                            output.writeEndElement();
294
+                        } else if(query.value(1) == "LOSS") {
295
+                            output.writeTextElement("td", query.value(9));
296
+                        } else if(query.value(1) == "SALE") {
297
+                            output.writeTextElement("td", query.value(8));
298
+                        } else {
299
+                            output.writeTextElement("td", "");
300
+                        }
301
+						output.writeTextElement("td", query.value(10));
302
+                        output.writeEndElement(); // tr
303
+                    }
304
+                    output.writeEndElement(); // table
305
+                    /* Put the rest of the report here. No sense running queries if
306
+                    the item doesn't exist. */
307
+                } else {
308
+                    /* Close tags if item data not found. */
309
+                    output.writeEndElement(); // tr
310
+                    output.writeEndElement(); // table
311
+                }
312
+                output.writeEndElement(); // body
313
+                output.writeEndElement(); // html
314
+                output.writeEndDocument();
315
+                view.setContent(buffer);
316
+                buffer.close();
317
+                query = query.invalidate();
318
+            }
319
+            if(itemBox.currentData() > 0) {
320
+                refresh();
321
+            }
322
+            /* Open invoices */
323
+            var openInvoice = function(url) {
324
+                var arg = url.slice(1, url.length);
325
+                var info = createWindow("invoiceinfo");
326
+                info.setInvoiceID(arg);
327
+                var query = new QSqlQuery();
328
+                query.exec("SELECT time, invoice, vendor FROM invoices WHERE id = " + arg);
329
+                query.next();
330
+                var timefield = findChildObject(info, 'date');
331
+                timefield.text = query.value(0);
332
+                var vendorfield = findChildObject(info, 'vendor');
333
+                vendorfield.text = query.value(2);
334
+                var invoicefield = findChildObject(info, 'invoice');
335
+                invoicefield.text = query.value(1);
336
+                var itemtable = findChildObject(info, 'itemtable');
337
+                itemtable.setQuery("SELECT record_type, item_id, description, (SELECT reference FROM items WHERE id = item_id) AS reference, (SELECT cost FROM purchase WHERE item = item_id) AS unit_cost, (SELECT quantity FROM purchase WHERE item = item_id) AS quantity, ((SELECT quantity FROM purchase WHERE item = item_id)/(SELECT conversion FROM lb_bag_conversion WHERE item = item_id))::numeric(12,2) AS sacks, cost FROM invoice_items WHERE invoice_id = " + arg + " AND record_type = 'PURCHASE' UNION SELECT record_type, NULL, description, NULL, NULL, NULL, NULL, cost FROM invoice_items WHERE invoice_id = " + arg + " AND record_type = 'FEE' ORDER BY item_id");
338
+                query = query.invalidate();
339
+            };
340
+            /* Open batch data */
341
+            var openProfile = function(url) {
342
+                var arg = url.slice(1, url.length);
343
+                var details = createWindow("batchDetails");
344
+                var fakeTable = new Object;
345
+                fakeTable.holding = new Array(7);
346
+                fakeTable.data = function(r, c) {
347
+                    return this.holding[c];
348
+                };
349
+                var query = new QSqlQuery();
350
+                query.exec("SELECT time, machine, (SELECT name FROM items WHERE id = roasted_id) AS name, unroasted_total_quantity AS green, roasted_quantity AS roasted, ((unroasted_total_quantity - roasted_quantity) / unroasted_total_quantity * 100::numeric)::numeric(12,2) AS weight_loss, duration, annotation FROM roasting_log WHERE files = '{" + arg + "}'");
351
+                query.next();
352
+                for(var i = 0; i < 8; i++) {
353
+                    fakeTable.holding[i] = query.value(i);
354
+                }
355
+                query = query.invalidate();
356
+                details.loadData(fakeTable, 0);
357
+            };
358
+            view.scriptLinkClicked.connect(function(url) {
359
+                var linkType = url[0];
360
+                switch(linkType) {
361
+                    case 'i':
362
+                        openInvoice(url);
363
+                        break;
364
+                    case 'p':
365
+                        openProfile(url);
366
+                        break;
367
+                }
368
+            });
369
+        ]]>
370
+    </program>
325 371
 </window>

+ 38
- 0
config/Scripts/batchtag.css View File

@@ -0,0 +1,38 @@
1
+body, h1, h2, h3, h4, h5, h6,
2
+p, blockquote, pre, hr,
3
+dl, dd, ol, ul, figure {
4
+  margin: 0;
5
+  padding: 0;
6
+}
7
+*,
8
+*::before,
9
+*::after {
10
+    -webkit-box-sizing: border-box;
11
+    -moz-box-sizing: border-box;
12
+    box-sizing: border-box;
13
+}
14
+
15
+body {
16
+  width: 58mm;
17
+}
18
+
19
+h1 {
20
+  padding-top: 10mm;
21
+  font-weight: 400;
22
+  font-size: 5mm;
23
+  text-align: center;
24
+}
25
+
26
+span {
27
+  display: block;
28
+  font-size: 4mm;
29
+  margin-left: 1mm;
30
+  margin-right: 1mm;
31
+}
32
+
33
+#container {
34
+	width: 190px;
35
+	margin-left: auto;
36
+	margin-right: auto;
37
+}
38
+

+ 346
- 0
config/Scripts/qrcode.js View File

@@ -0,0 +1,346 @@
1
+/**
2
+ * @fileoverview
3
+ * - modified davidshimjs/qrcodejs library for use in node.js
4
+ * - Using the 'QRCode for Javascript library'
5
+ * - Fixed dataset of 'QRCode for Javascript library' for support full-spec.
6
+ * - this library has no dependencies.
7
+ *
8
+ * @version 0.9.1 (2016-02-12)
9
+ * @author davidshimjs, papnkukn
10
+ * @see <a href="http://www.d-project.com/" target="_blank">http://www.d-project.com/</a>
11
+ * @see <a href="http://jeromeetienne.github.com/jquery-qrcode/" target="_blank">http://jeromeetienne.github.com/jquery-qrcode/</a>
12
+ * @see <a href="https://github.com/davidshimjs/qrcodejs" target="_blank">https://github.com/davidshimjs/qrcodejs</a>
13
+ */
14
+
15
+//---------------------------------------------------------------------
16
+// QRCode for JavaScript
17
+//
18
+// Copyright (c) 2009 Kazuhiko Arase
19
+//
20
+// URL: http://www.d-project.com/
21
+//
22
+// Licensed under the MIT license:
23
+//   http://www.opensource.org/licenses/mit-license.php
24
+//
25
+// The word "QR Code" is registered trademark of 
26
+// DENSO WAVE INCORPORATED
27
+//   http://www.denso-wave.com/qrcode/faqpatent-e.html
28
+//
29
+// Modified by Neal Wilson to replace the width and height attributes
30
+// of the generated svg with a viewbox for easier scaling.
31
+//
32
+//---------------------------------------------------------------------
33
+function QR8bitByte(data) {
34
+  this.mode = QRMode.MODE_8BIT_BYTE;
35
+  this.data = data;
36
+  this.parsedData = [];
37
+
38
+  // Added to support UTF-8 Characters
39
+  for (var i = 0, l = this.data.length; i < l; i++) {
40
+    var byteArray = [];
41
+    var code = this.data.charCodeAt(i);
42
+
43
+    if (code > 0x10000) {
44
+      byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18);
45
+      byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12);
46
+      byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6);
47
+      byteArray[3] = 0x80 | (code & 0x3F);
48
+    } else if (code > 0x800) {
49
+      byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12);
50
+      byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6);
51
+      byteArray[2] = 0x80 | (code & 0x3F);
52
+    } else if (code > 0x80) {
53
+      byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6);
54
+      byteArray[1] = 0x80 | (code & 0x3F);
55
+    } else {
56
+      byteArray[0] = code;
57
+    }
58
+
59
+    this.parsedData.push(byteArray);
60
+  }
61
+
62
+  this.parsedData = Array.prototype.concat.apply([], this.parsedData);
63
+
64
+  if (this.parsedData.length != this.data.length) {
65
+    this.parsedData.unshift(191);
66
+    this.parsedData.unshift(187);
67
+    this.parsedData.unshift(239);
68
+  }
69
+}
70
+
71
+QR8bitByte.prototype = {
72
+  getLength: function (buffer) {
73
+    return this.parsedData.length;
74
+  },
75
+  write: function (buffer) {
76
+    for (var i = 0, l = this.parsedData.length; i < l; i++) {
77
+      buffer.put(this.parsedData[i], 8);
78
+    }
79
+  }
80
+};
81
+
82
+function QRCodeModel(typeNumber, errorCorrectLevel) {
83
+  this.typeNumber = typeNumber;
84
+  this.errorCorrectLevel = errorCorrectLevel;
85
+  this.modules = null;
86
+  this.moduleCount = 0;
87
+  this.dataCache = null;
88
+  this.dataList = [];
89
+}
90
+
91
+QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);}
92
+return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row<this.moduleCount;row++){this.modules[row]=new Array(this.moduleCount);for(var col=0;col<this.moduleCount;col++){this.modules[row][col]=null;}}
93
+this.setupPositionProbePattern(0,0);this.setupPositionProbePattern(this.moduleCount-7,0);this.setupPositionProbePattern(0,this.moduleCount-7);this.setupPositionAdjustPattern();this.setupTimingPattern();this.setupTypeInfo(test,maskPattern);if(this.typeNumber>=7){this.setupTypeNumber(test);}
94
+if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);}
95
+this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}}
96
+return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row<this.modules.length;row++){var y=row*cs;for(var col=0;col<this.modules[row].length;col++){var x=col*cs;var dark=this.modules[row][col];if(dark){qr_mc.beginFill(0,100);qr_mc.moveTo(x,y);qr_mc.lineTo(x+cs,y);qr_mc.lineTo(x+cs,y+cs);qr_mc.lineTo(x,y+cs);qr_mc.endFill();}}}
97
+return qr_mc;},setupTimingPattern:function(){for(var r=8;r<this.moduleCount-8;r++){if(this.modules[r][6]!=null){continue;}
98
+this.modules[r][6]=(r%2==0);}
99
+for(var c=8;c<this.moduleCount-8;c++){if(this.modules[6][c]!=null){continue;}
100
+this.modules[6][c]=(c%2==0);}},setupPositionAdjustPattern:function(){var pos=QRUtil.getPatternPosition(this.typeNumber);for(var i=0;i<pos.length;i++){for(var j=0;j<pos.length;j++){var row=pos[i];var col=pos[j];if(this.modules[row][col]!=null){continue;}
101
+for(var r=-2;r<=2;r++){for(var c=-2;c<=2;c++){if(r==-2||r==2||c==-2||c==2||(r==0&&c==0)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}}}},setupTypeNumber:function(test){var bits=QRUtil.getBCHTypeNumber(this.typeNumber);for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;}
102
+for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}}
103
+for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}}
104
+this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex<data.length){dark=(((data[byteIndex]>>>bitIndex)&1)==1);}
105
+var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;}
106
+this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}}
107
+row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;i<dataList.length;i++){var data=dataList[i];buffer.put(data.mode,4);buffer.put(data.getLength(),QRUtil.getLengthInBits(data.mode,typeNumber));data.write(buffer);}
108
+var totalDataCount=0;for(var i=0;i<rsBlocks.length;i++){totalDataCount+=rsBlocks[i].dataCount;}
109
+if(buffer.getLengthInBits()>totalDataCount*8){throw new Error("code length overflow. ("
110
++buffer.getLengthInBits()
111
++">"
112
++totalDataCount*8
113
++")");}
114
+if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);}
115
+while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);}
116
+while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;}
117
+buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;}
118
+buffer.put(QRCodeModel.PAD1,8);}
119
+return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r<rsBlocks.length;r++){var dcCount=rsBlocks[r].dataCount;var ecCount=rsBlocks[r].totalCount-dcCount;maxDcCount=Math.max(maxDcCount,dcCount);maxEcCount=Math.max(maxEcCount,ecCount);dcdata[r]=new Array(dcCount);for(var i=0;i<dcdata[r].length;i++){dcdata[r][i]=0xff&buffer.buffer[i+offset];}
120
+offset+=dcCount;var rsPoly=QRUtil.getErrorCorrectPolynomial(ecCount);var rawPoly=new QRPolynomial(dcdata[r],rsPoly.getLength()-1);var modPoly=rawPoly.mod(rsPoly);ecdata[r]=new Array(rsPoly.getLength()-1);for(var i=0;i<ecdata[r].length;i++){var modIndex=i+modPoly.getLength()-ecdata[r].length;ecdata[r][i]=(modIndex>=0)?modPoly.get(modIndex):0;}}
121
+var totalCodeCount=0;for(var i=0;i<rsBlocks.length;i++){totalCodeCount+=rsBlocks[i].totalCount;}
122
+var data=new Array(totalCodeCount);var index=0;for(var i=0;i<maxDcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<dcdata[r].length){data[index++]=dcdata[r][i];}}}
123
+for(var i=0;i<maxEcCount;i++){for(var r=0;r<rsBlocks.length;r++){if(i<ecdata[r].length){data[index++]=ecdata[r][i];}}}
124
+return data;};var QRMode={MODE_NUMBER:1<<0,MODE_ALPHA_NUM:1<<1,MODE_8BIT_BYTE:1<<2,MODE_KANJI:1<<3};var QRErrorCorrectLevel={L:1,M:0,Q:3,H:2};var QRMaskPattern={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7};var QRUtil={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:(1<<10)|(1<<8)|(1<<5)|(1<<4)|(1<<2)|(1<<1)|(1<<0),G18:(1<<12)|(1<<11)|(1<<10)|(1<<9)|(1<<8)|(1<<5)|(1<<2)|(1<<0),G15_MASK:(1<<14)|(1<<12)|(1<<10)|(1<<4)|(1<<1),getBCHTypeInfo:function(data){var d=data<<10;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)>=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));}
125
+return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));}
126
+return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;}
127
+return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i<errorCorrectLength;i++){a=a.multiply(new QRPolynomial([1,QRMath.gexp(i)],0));}
128
+return a;},getLengthInBits:function(mode,type){if(1<=type&&type<10){switch(mode){case QRMode.MODE_NUMBER:return 10;case QRMode.MODE_ALPHA_NUM:return 9;case QRMode.MODE_8BIT_BYTE:return 8;case QRMode.MODE_KANJI:return 8;default:throw new Error("mode:"+mode);}}else if(type<27){switch(mode){case QRMode.MODE_NUMBER:return 12;case QRMode.MODE_ALPHA_NUM:return 11;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 10;default:throw new Error("mode:"+mode);}}else if(type<41){switch(mode){case QRMode.MODE_NUMBER:return 14;case QRMode.MODE_ALPHA_NUM:return 13;case QRMode.MODE_8BIT_BYTE:return 16;case QRMode.MODE_KANJI:return 12;default:throw new Error("mode:"+mode);}}else{throw new Error("type:"+type);}},getLostPoint:function(qrCode){var moduleCount=qrCode.getModuleCount();var lostPoint=0;for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount;col++){var sameCount=0;var dark=qrCode.isDark(row,col);for(var r=-1;r<=1;r++){if(row+r<0||moduleCount<=row+r){continue;}
129
+for(var c=-1;c<=1;c++){if(col+c<0||moduleCount<=col+c){continue;}
130
+if(r==0&&c==0){continue;}
131
+if(dark==qrCode.isDark(row+r,col+c)){sameCount++;}}}
132
+if(sameCount>5){lostPoint+=(3+sameCount-5);}}}
133
+for(var row=0;row<moduleCount-1;row++){for(var col=0;col<moduleCount-1;col++){var count=0;if(qrCode.isDark(row,col))count++;if(qrCode.isDark(row+1,col))count++;if(qrCode.isDark(row,col+1))count++;if(qrCode.isDark(row+1,col+1))count++;if(count==0||count==4){lostPoint+=3;}}}
134
+for(var row=0;row<moduleCount;row++){for(var col=0;col<moduleCount-6;col++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row,col+1)&&qrCode.isDark(row,col+2)&&qrCode.isDark(row,col+3)&&qrCode.isDark(row,col+4)&&!qrCode.isDark(row,col+5)&&qrCode.isDark(row,col+6)){lostPoint+=40;}}}
135
+for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount-6;row++){if(qrCode.isDark(row,col)&&!qrCode.isDark(row+1,col)&&qrCode.isDark(row+2,col)&&qrCode.isDark(row+3,col)&&qrCode.isDark(row+4,col)&&!qrCode.isDark(row+5,col)&&qrCode.isDark(row+6,col)){lostPoint+=40;}}}
136
+var darkCount=0;for(var col=0;col<moduleCount;col++){for(var row=0;row<moduleCount;row++){if(qrCode.isDark(row,col)){darkCount++;}}}
137
+var ratio=Math.abs(100*darkCount/moduleCount/moduleCount-50)/5;lostPoint+=ratio*10;return lostPoint;}};var QRMath={glog:function(n){if(n<1){throw new Error("glog("+n+")");}
138
+return QRMath.LOG_TABLE[n];},gexp:function(n){while(n<0){n+=255;}
139
+while(n>=256){n-=255;}
140
+return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<<i;}
141
+for(var i=8;i<256;i++){QRMath.EXP_TABLE[i]=QRMath.EXP_TABLE[i-4]^QRMath.EXP_TABLE[i-5]^QRMath.EXP_TABLE[i-6]^QRMath.EXP_TABLE[i-8];}
142
+for(var i=0;i<255;i++){QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]]=i;}
143
+function QRPolynomial(num,shift){if(num.length==undefined){throw new Error(num.length+"/"+shift);}
144
+var offset=0;while(offset<num.length&&num[offset]==0){offset++;}
145
+this.num=new Array(num.length-offset+shift);for(var i=0;i<num.length-offset;i++){this.num[i]=num[i+offset];}}
146
+QRPolynomial.prototype={get:function(index){return this.num[index];},getLength:function(){return this.num.length;},multiply:function(e){var num=new Array(this.getLength()+e.getLength()-1);for(var i=0;i<this.getLength();i++){for(var j=0;j<e.getLength();j++){num[i+j]^=QRMath.gexp(QRMath.glog(this.get(i))+QRMath.glog(e.get(j)));}}
147
+return new QRPolynomial(num,0);},mod:function(e){if(this.getLength()-e.getLength()<0){return this;}
148
+var ratio=QRMath.glog(this.get(0))-QRMath.glog(e.get(0));var num=new Array(this.getLength());for(var i=0;i<this.getLength();i++){num[i]=this.get(i);}
149
+for(var i=0;i<e.getLength();i++){num[i]^=QRMath.gexp(QRMath.glog(e.get(i))+ratio);}
150
+return new QRPolynomial(num,0).mod(e);}};function QRRSBlock(totalCount,dataCount){this.totalCount=totalCount;this.dataCount=dataCount;}
151
+QRRSBlock.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]];QRRSBlock.getRSBlocks=function(typeNumber,errorCorrectLevel){var rsBlock=QRRSBlock.getRsBlockTable(typeNumber,errorCorrectLevel);if(rsBlock==undefined){throw new Error("bad rs block @ typeNumber:"+typeNumber+"/errorCorrectLevel:"+errorCorrectLevel);}
152
+var length=rsBlock.length/3;var list=[];for(var i=0;i<length;i++){var count=rsBlock[i*3+0];var totalCount=rsBlock[i*3+1];var dataCount=rsBlock[i*3+2];for(var j=0;j<count;j++){list.push(new QRRSBlock(totalCount,dataCount));}}
153
+return list;};QRRSBlock.getRsBlockTable=function(typeNumber,errorCorrectLevel){switch(errorCorrectLevel){case QRErrorCorrectLevel.L:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+0];case QRErrorCorrectLevel.M:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+1];case QRErrorCorrectLevel.Q:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+2];case QRErrorCorrectLevel.H:return QRRSBlock.RS_BLOCK_TABLE[(typeNumber-1)*4+3];default:return undefined;}};function QRBitBuffer(){this.buffer=[];this.length=0;}
154
+QRBitBuffer.prototype={get:function(index){var bufIndex=Math.floor(index/8);return((this.buffer[bufIndex]>>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i<length;i++){this.putBit(((num>>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);}
155
+if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));}
156
+this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]];
157
+
158
+
159
+/** Constructor */
160
+function QRCode(options) {
161
+  var instance = this;
162
+  
163
+  //Default options
164
+  this.options = {
165
+    padding: 4,
166
+    width: 256, 
167
+    height: 256,
168
+    typeNumber: 4,
169
+    color: "#000000",
170
+    background: "#ffffff",
171
+    ecl: "M"
172
+  };
173
+  
174
+  //In case the options is string
175
+  if (typeof options === 'string') {
176
+    options = {
177
+      content: options
178
+    };
179
+  }
180
+  
181
+  //Merge options
182
+  if (options) {
183
+    for (var i in options) {
184
+      this.options[i] = options[i];
185
+    }
186
+  }
187
+  
188
+  if (typeof this.options.content !== 'string') {
189
+    throw new Error("Expected 'content' as string!");
190
+  }
191
+  
192
+  if (this.options.content.length === 0 /* || this.options.content.length > 7089 */) {
193
+    throw new Error("Expected 'content' to be non-empty!");
194
+  }
195
+  
196
+  if (!(this.options.padding >= 0)) {
197
+    throw new Error("Expected 'padding' value to be non-negative!");
198
+  }
199
+  
200
+  if (!(this.options.width > 0) || !(this.options.height > 0)) {
201
+    throw new Error("Expected 'width' or 'height' value to be higher than zero!");
202
+  }
203
+  
204
+  //Gets the error correction level
205
+  function _getErrorCorrectLevel(ecl) {
206
+    switch (ecl) {
207
+        case "L":
208
+          return QRErrorCorrectLevel.L;
209
+          
210
+        case "M":
211
+          return QRErrorCorrectLevel.M;
212
+          
213
+        case "Q":
214
+          return QRErrorCorrectLevel.Q;
215
+          
216
+        case "H":
217
+          return QRErrorCorrectLevel.H;
218
+          
219
+        default:
220
+          throw new Error("Unknwon error correction level: " + ecl);
221
+      }
222
+  }
223
+  
224
+  //Get type number
225
+  function _getTypeNumber(content, ecl) {      
226
+    var length = _getUTF8Length(content);
227
+    
228
+    var type = 1;
229
+    var limit = 0;
230
+    for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) {
231
+      var table = QRCodeLimitLength[i];
232
+      if (!table) {
233
+        throw new Error("Content too long: expected " + limit + " but got " + length);
234
+      }
235
+      
236
+      switch (ecl) {
237
+        case "L":
238
+          limit = table[0];
239
+          break;
240
+          
241
+        case "M":
242
+          limit = table[1];
243
+          break;
244
+          
245
+        case "Q":
246
+          limit = table[2];
247
+          break;
248
+          
249
+        case "H":
250
+          limit = table[3];
251
+          break;
252
+          
253
+        default:
254
+          throw new Error("Unknwon error correction level: " + ecl);
255
+      }
256
+      
257
+      if (length <= limit) {
258
+        break;
259
+      }
260
+      
261
+      type++;
262
+    }
263
+    
264
+    if (type > QRCodeLimitLength.length) {
265
+      throw new Error("Content too long");
266
+    }
267
+    
268
+    return type;
269
+  }
270
+
271
+  //Gets text length
272
+  function _getUTF8Length(content) {
273
+    var result = encodeURI(content).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a');
274
+    return result.length + (result.length != content ? 3 : 0);
275
+  }
276
+  
277
+  //Generate QR Code matrix
278
+  var content = this.options.content;
279
+  var type = _getTypeNumber(content, this.options.ecl);
280
+  var ecl = _getErrorCorrectLevel(this.options.ecl);
281
+  this.qrcode = new QRCodeModel(type, ecl);
282
+  this.qrcode.addData(content);
283
+  this.qrcode.make();
284
+}
285
+
286
+/** Generates QR Code as SVG image */
287
+QRCode.prototype.svg = function(opt) {
288
+  if (typeof opt == "undefined") {
289
+    opt = { container: "svg" };
290
+  }
291
+  
292
+  var options = this.options;
293
+  var modules = this.qrcode.modules;
294
+  
295
+  var EOL = '\r\n';
296
+  var width = options.width;
297
+  var height = options.height;
298
+  var length = modules.length;
299
+  var xsize = width / (length + 2 * options.padding);
300
+  var ysize = height / (length + 2 * options.padding);
301
+  
302
+  var rect = '<rect x="0" y="0" width="' + width + '" height="' + height + '" style="fill:' + options.background + ';shape-rendering:crispEdges;"/>' + EOL;
303
+
304
+  for (var y = 0; y < length; y++) {
305
+    for (var x = 0; x < length; x++) {
306
+      var module = modules[x][y];
307
+      if (module) {
308
+        var px = (x * xsize + options.padding * xsize).toString();
309
+        var py = (y * ysize + options.padding * ysize).toString();
310
+        rect += '<rect x="' + px + '" y="' + py + '" width="' + xsize + '" height="' + ysize + '" style="fill:' + options.color + ';shape-rendering:crispEdges;"/>' + EOL;
311
+      }
312
+    }
313
+  }
314
+
315
+  var svg = "";
316
+  switch (opt.container) {
317
+    case "svg":
318
+      svg += '<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewbox="0 0 ' + width + ' ' + height + '">' + EOL;
319
+      svg += rect;
320
+      svg += '</svg>';
321
+      break;
322
+      
323
+    case "g":
324
+      svg += '<g width="' + width + '" height="' + height + '">' + EOL;
325
+      svg += rect;
326
+      svg += '</g>';
327
+      break;
328
+      
329
+    default:
330
+      svg += rect;
331
+      break;
332
+  }
333
+  
334
+  return svg;
335
+};
336
+
337
+/** Writes QR Code image to a file */
338
+QRCode.prototype.save = function(file, callback) {
339
+  var data = this.svg();
340
+  var fs = require('fs');
341
+  fs.writeFile(file, data, callback);
342
+};
343
+
344
+if (typeof module != "undefined") {
345
+  module.exports = QRCode;
346
+}

BIN
config/Translations/config.de.qm View File


+ 1982
- 1782
config/Translations/config.de.ts
File diff suppressed because it is too large
View File


+ 3008
- 2307
config/Translations/config.ts
File diff suppressed because it is too large
View File


+ 7
- 5
config/Windows/greeninventory.xml View File

@@ -65,8 +65,8 @@
65 65
             button.clicked.connect(function() {
66 66
                 q = "INSERT INTO ";
67 67
                 q += (types.currentIndex == 0 ?
68
-                    "inventory (time, item, quantity)" :
69
-                    "loss (time, item, quantity, reason)");
68
+                    "inventory (time, item, quantity, person)" :
69
+                    "loss (time, item, quantity, reason, person)");
70 70
                 q += " VALUES ('now', ";
71 71
                 q = q + items.currentData();
72 72
                 q = q + ", ";
@@ -82,10 +82,12 @@
82 82
                     q = q + ")";
83 83
                 }
84 84
                 q += (types.currentIndex == 0 ?
85
-                    ")" :
86
-                    ", '" + reason.text + "')");
85
+                    ", :user)" :
86
+                    ", '" + reason.text + "', :user)");
87 87
                 query = new QSqlQuery();
88
-                query.exec(q);
88
+                query.prepare(q);
89
+				query.bind(":user", Application.currentTypicaUser());
90
+				query.exec();
89 91
                 updateStatus();
90 92
             });
91 93
             items['currentIndexChanged(int)'].connect(updateStatus);

+ 2
- 1
config/Windows/greensales.xml View File

@@ -53,7 +53,7 @@
53 53
 			var submit = findChildObject(this, 'submit');
54 54
 			submit.clicked.connect(function() {
55 55
 				var query = new QSqlQuery();
56
-				query.prepare("INSERT INTO sale (time, item, quantity, customer) VALUES(:time, :item, :quantity, :customer)");
56
+				query.prepare("INSERT INTO sale (time, item, quantity, customer, person) VALUES(:time, :item, :quantity, :customer, :user)");
57 57
 				query.bind(":time", dateField.text);
58 58
 				if(customerField.text == "") {
59 59
 					query.bind(":customer", null);
@@ -62,6 +62,7 @@
62 62
 					query.bind(":customer", customerField.text);
63 63
 				}
64 64
 				var coffeesArray = sqlToArray(items.columnArray(0, 32));
65
+				query.bind(":user", Application.currentTypicaUser());
65 66
 				if(coffeesArray.length > 0)
66 67
 				{
67 68
 					for(var i = 0; i < coffeesArray.length; i++)

+ 496
- 0
config/Windows/manuallogentry.xml View File

@@ -0,0 +1,496 @@
1
+<window id="manualLogEntry">
2
+	<menu name="File">
3
+		<plugins id="pluginMenu" title="Import" src="ImportFilters" preRun="pluginContext.preRun();" postRun="pluginContext.postRun();"/>
4
+		<separator />
5
+		<item id="quitItem" shortcut="ctrl+Q">Quit</item>
6
+	</menu>
7
+	<menu name="Log">
8
+        <item id="clear" shortcut="Ctrl+L">Clear Log</item>
9
+        <separator />
10
+        <item id="ms">Millisecond View</item>
11
+        <item id="1s">1 Second View</item>
12
+        <item id="5s">5 Second View</item>
13
+        <item id="10s">10 Second View</item>
14
+        <item id="15s">15 Second View</item>
15
+        <item id="30s">30 Second View</item>
16
+        <item id="1m">1 Minute View</item>
17
+		<separator />
18
+		<item id="showC">Display Celsius</item>
19
+		<item id="showF">Display Fahrenheit</item>
20
+    </menu>
21
+	<layout type="vertical">
22
+		<tabbar id="tabs"/>
23
+		<layout type="stack" id="pages">
24
+			<page>
25
+				<layout type="vertical">
26
+					<layout type="horizontal">
27
+						<label>Batch Type:</label>
28
+						<sqldrop id="batchType" />
29
+						<label>Machine:</label>
30
+						<sqldrop id="machineSelector" />
31
+						<stretch />
32
+					</layout>
33
+					<label>Green Coffee:</label>
34
+					<layout type="stack" id="greenInfoLayout">
35
+						<page id="sampleGreen">
36
+							<layout type="vertical">
37
+								<layout type="grid">
38
+									<row>
39
+										<column><label>Name:</label></column>
40
+										<column><line id="sampleGreenName" /></column>
41
+									</row>
42
+									<row>
43
+										<column><label>Weight:</label></column>
44
+										<column><line id="sampleGreenWeight" validator="numeric">0.0</line></column>
45
+										<column><sqldrop id="sampleGreenUnit" /></column>
46
+									</row>
47
+									<row>
48
+										<column><label>Vendor:</label></column>
49
+										<column><line id="sampleGreenVendor" /></column>
50
+									</row>
51
+									<row>
52
+										<column><label>Arrival Date:</label></column>
53
+										<column><calendar id="sampleGreenArrivalDate" /></column>
54
+									</row>
55
+								</layout>
56
+								<label>Additional Details:</label>
57
+								<sqltablearray columns="2" id="attributes">
58
+									<column name="Attribute" />
59
+									<column name="Value" />
60
+								</sqltablearray>
61
+							</layout>
62
+						</page>
63
+						<page id="productionGreen">
64
+							<layout type="vertical">
65
+								<layout type="horizontal">
66
+									<label>Unit:</label>
67
+									<sqldrop id="productionGreenUnit" />
68
+									<stretch />
69
+								</layout>
70
+								<sqltablearray columns="2" id="productionGreenTable">
71
+									<column name="Coffee" delegate="sql" showdata="true" null="true" nulltext="Delete" nulldata="delete" data="0" display="1">
72
+										<![CDATA[SELECT id, name FROM coffees WHERE quantity <> 0 ORDER BY name]]>
73
+									</column>
74
+									<column name="Weight" delegate="numeric" />
75
+								</sqltablearray>
76
+							</layout>
77
+						</page>
78
+					</layout>
79
+					<label>Roasting Details:</label>
80
+					<layout type="grid">
81
+						<row>
82
+							<column><label>Item:</label></column>
83
+							<column>
84
+								<sqldrop data="0" display="1" showdata="true" id="roastedItem">
85
+									<null />
86
+									<query>SELECT id, name FROM items WHERE category = 'Coffee: Roasted' AND id IN (SELECT item FROM current_items) ORDER BY name</query>
87
+								</sqldrop>
88
+							</column>
89
+						</row>
90
+						<row>
91
+							<column><label>Weight:</label></column>
92
+							<column><line id="roastedWeight" validator="numeric">0.0</line></column>
93
+						</row>
94
+						<row>
95
+							<column><label>Time:</label></column>
96
+							<column><calendar id="roastTime" time="true"/></column>
97
+						</row>
98
+						<row>
99
+							<column><label>Duration:</label></column>
100
+							<column><timeedit id="roastDuration" /></column>
101
+						</row>
102
+						<row>
103
+							<column><label>Notes:</label></column>
104
+							<column><textarea id="notes" /></column>
105
+						</row>
106
+					</layout>
107
+				</layout>
108
+			</page>
109
+			<page>
110
+				<layout type="vertical">
111
+					<layout type="horizontal">
112
+						<label>Time Increment (s):</label>
113
+						<line id="timeincrement" validator="numeric">30</line>
114
+						<stretch />
115
+						<label>Time:</label>
116
+						<timeedit id="currenttime" />
117
+						<stretch />
118
+						<label>Temperature:</label>
119
+						<line id="currenttemperature" validator="numeric" />
120
+						<label>Note:</label>
121
+						<line id="currentnote" />
122
+						<button name="Add Measurement" id="addmeasurement" type="push" />
123
+					</layout>
124
+					<splitter type="horizontal" id="roastdatasplit">
125
+						<measurementtable id="log" />
126
+						<graph id="graph" />
127
+					</splitter>
128
+				</layout>
129
+			</page>
130
+		</layout>
131
+		<layout type="horizontal">
132
+			<stretch />
133
+			<button name="Submit" id="submit" type="push" />
134
+		</layout>
135
+	</layout>
136
+	<program>
137
+	<![CDATA[
138
+		var window = this;
139
+		this.windowTitle = "Typica - Manual Log Entry";
140
+		window.windowReady.connect(function() {
141
+			if(machineModel.rowCount() == 0) {
142
+				displayError(TTR("manualLogEntry", "Configuration Required"),
143
+				TTR("manualLogEntry", "Please configure a roaster."));
144
+				window.close();
145
+			}
146
+		});
147
+		quitItem = findChildObject(this, 'quitItem');
148
+		quitItem.triggered.connect(function() {
149
+			Application.quit();
150
+		});
151
+		pluginContext = {};
152
+		pluginContext.table = findChildObject(this, 'log');
153
+		pluginContext.table.setHeaderData(1, "Temp");
154
+		pluginContext.table.addOutputTemperatureColumn(1);
155
+		pluginContext.table.setHeaderData(2, "Note");
156
+		pluginContext.table.addOutputAnnotationColumn(2);
157
+		pluginContext.graph = findChildObject(this, 'graph');
158
+		pluginContext.preRun = function() {
159
+			var filename = QFileDialog.getOpenFileName(window, TTR("manualLogEntry", "Import"), QSettings.value('script/lastDir', '') + '/');
160
+			var file = new QFile(filename);
161
+			if(file.open(1)) {
162
+				pluginContext.data = file.readToString();
163
+				file.close();
164
+				pluginContext.table.clear();
165
+				pluginContext.graph.clear();
166
+				QSettings.setValue("script/lastDir", dir(filename));
167
+			} else {
168
+				throw new Error("Failed to open file, aborting import.");
169
+			}
170
+		};
171
+		pluginContext.postRun = function() {
172
+			
173
+		};
174
+		pluginContext.newMeasurement = function(m, c) {
175
+			pluginContext.table.newMeasurement(m, c);
176
+			pluginContext.graph.newMeasurement(m, c);
177
+		}
178
+		pluginMenu = findChildObject(this, 'pluginMenu');
179
+		pluginMenu.setProperty("activationObject", pluginContext);
180
+		tabs = findChildObject(this, 'tabs');
181
+		tabs.addTab("Batch Data");
182
+		tabs.addTab("Roast Data");
183
+		pages = findChildObject(this, 'pages');
184
+		tabs.currentChanged.connect(function(index) {
185
+			pages.setCurrentIndex(index);
186
+		});
187
+		greenInfoLayout = findChildObject(this, 'greenInfoLayout');
188
+		roastedItem = findChildObject(this, 'roastedItem');
189
+		batchType = findChildObject(this, 'batchType');
190
+		batchType.addItem("Sample");
191
+		batchType.addItem("Production");
192
+		batchType['currentIndexChanged(int)'].connect(function(batchTypeIndex) {
193
+			QSettings.setValue("script/manual_batchType", batchTypeIndex);
194
+			greenInfoLayout.setCurrentIndex(batchTypeIndex);
195
+			roastedItem.enabled = (batchTypeIndex == 1);
196
+		});
197
+		batchType.setCurrentIndex(QSettings.value("script/manual_batchType", 1));
198
+		var machineSelector = findChildObject(this, 'machineSelector');
199
+        var machineModel = new DeviceTreeModel;
200
+        machineSelector.setModel(machineModel);
201
+        machineSelector.currentIndex = QSettings.value("script/manualMachineSelection", 0);
202
+        machineSelector['currentIndexChanged(int)'].connect(function(index) {
203
+            QSettings.setValue("script/manualMachineSelection", index);
204
+        });
205
+		sampleGreenUnit = findChildObject(this, 'sampleGreenUnit');
206
+		sampleGreenUnit.addItem("g");
207
+		sampleGreenUnit.addItem("Kg");
208
+		sampleGreenUnit.addItem("oz");
209
+		sampleGreenUnit.addItem("lb");
210
+		sampleGreenUnit.currentIndex = (QSettings.value("script/manual_unit", sampleGreenUnit.findText("lb")));
211
+		productionGreenUnit = findChildObject(this, 'productionGreenUnit');
212
+		productionGreenUnit.addItem("g");
213
+		productionGreenUnit.addItem("Kg");
214
+		productionGreenUnit.addItem("oz");
215
+		productionGreenUnit.addItem("lb");
216
+		productionGreenUnit.currentIndex = (QSettings.value("script/manual_unit", productionGreenUnit.findText("lb")));
217
+		sampleGreenUnit['currentIndexChanged(int)'].connect(function(greenUnitIndex) {
218
+			QSettings.setValue("script/manual_unit", greenUnitIndex);
219
+			productionGreenUnit.setCurrentIndex(greenUnitIndex);
220
+		});
221
+		productionGreenUnit['currentIndexChanged(int)'].connect(function(greenUnitIndex) {
222
+			QSettings.setValue("script/manual_unit", greenUnitIndex);
223
+			sampleGreenUnit.setCurrentIndex(greenUnitIndex);
224
+		});
225
+		timeincrement = findChildObject(this, 'timeincrement');
226
+		currenttime = findChildObject(this, 'currenttime');
227
+		currenttemperature = findChildObject(this, 'currenttemperature');
228
+		currentnote = findChildObject(this, 'currentnote');
229
+		addmeasurement = findChildObject(this, 'addmeasurement');
230
+		var currentUnit = Units.Fahrenheit;
231
+		var showC = findChildObject(this, 'showC');
232
+        showC.triggered.connect(function() {
233
+            pluginContext.table.setDisplayUnits(Units.Celsius);
234
+            pluginContext.graph.showC();
235
+			QSettings.setValue("temperatureUnit", "C");
236
+			currentUnit = Units.Celsius;
237
+        });
238
+        var showF = findChildObject(this, 'showF');
239
+        showF.triggered.connect(function() {
240
+            pluginContext.table.setDisplayUnits(Units.Fahrenheit);
241
+            pluginContext.graph.showF();
242
+			QSettings.setValue("temperatureUnit", "F");
243
+			currentUnit = Units.Fahrenheit;
244
+        });
245
+		if(QSettings.value("temperatureUnit", "F") == "C") {
246
+			showC.trigger();
247
+		}
248
+		addmeasurement.clicked.connect(function() {
249
+			var fromUnit = 
250
+			pluginContext.newMeasurement(new Measurement(Units.convertTemperature(Number(currenttemperature.text), currentUnit, Units.Fahrenheit), currenttime.time), 1);
251
+			if(currentnote.text.length > 0) {
252
+				pluginContext.table.newAnnotation(currentnote.text, 1, 2);
253
+			}
254
+			currentnote.text = "";
255
+			var t = QTime();
256
+			t = t.fromString(currenttime.time, "hh:mm:ss");
257
+			t = t.addSecs(30);
258
+			currenttime.time = t;
259
+			currenttemperature.text = "";
260
+		});
261
+		currenttemperature.returnPressed.connect(addmeasurement.clicked);
262
+		currentnote.returnPressed.connect(addmeasurement.clicked);
263
+		var v1 = findChildObject(this, 'ms');
264
+        v1.triggered.connect(pluginContext.table.LOD_ms);
265
+        var v2 = findChildObject(this, '1s');
266
+        v2.triggered.connect(pluginContext.table.LOD_1s);
267
+        var v3 = findChildObject(this, '5s');
268
+        v3.triggered.connect(pluginContext.table.LOD_5s);
269
+        var v4 = findChildObject(this, '10s');
270
+        v4.triggered.connect(pluginContext.table.LOD_10s);
271
+        var v5 = findChildObject(this, '15s');
272
+        v5.triggered.connect(pluginContext.table.LOD_15s);
273
+        var v6 = findChildObject(this, '30s');
274
+        v6.triggered.connect(pluginContext.table.LOD_30s);
275
+        var v7 = findChildObject(this, '1m');
276
+        v7.triggered.connect(pluginContext.table.LOD_1m);
277
+		var clear = findChildObject(this, 'clear');
278
+        clear.triggered.connect(pluginContext.table.clear);
279
+        clear.triggered.connect(pluginContext.graph.clear);
280
+		clear.triggered.connect(function() {
281
+			currenttime.time = QTime(0, 0, 0, 0);
282
+			currenttemperature.text = "";
283
+			currentnote.text = "";
284
+			pluginContext.table.clearOutputColumns();
285
+			pluginContext.table.addOutputTemperatureColumn(1);
286
+			pluginContext.table.addOutputAnnotationColumn(2);
287
+		});
288
+		var sampleGreenName = findChildObject(this, 'sampleGreenName');
289
+		var sampleGreenWeight = findChildObject(this, 'sampleGreenWeight');
290
+		var productionGreenTable = findChildObject(this, 'productionGreenTable');
291
+		var greenModel = productionGreenTable.model();
292
+		var greenTotal = 0.0;
293
+		var updateGreenTable = function() {
294
+			var deleteRow = -1;
295
+			while((deleteRow = productionGreenTable.findData("delete", 0)) > -1) {
296
+				if(productionGreenTable.data(deleteRow, 0, 0) == "Delete") {
297
+					productionGreenTable.removeRow(productionGreenTable.findData("delete", 0));
298
+				} else {
299
+					break;
300
+				}
301
+			}
302
+			greenTotal = productionGreenTable.columnSum(1, 0);
303
+			productionGreenTable.resizeColumnToContents(0);
304
+		};
305
+		greenModel.dataChanged.connect(updateGreenTable);
306
+		var validateInputs = function() {
307
+			if(batchType.currentIndex == 0) {
308
+				/* Sample batch */
309
+				if(sampleGreenName.text.length == 0) {
310
+					tabs.setCurrentIndex(0);
311
+					displayError(TTR("manualLogEntry", "Data Entry Error"),
312
+					TTR("manualLogEntry", "Please enter a green coffee name."));
313
+					return false;
314
+				}
315
+				if(Number(sampleGreenWeight.text) <= 0 || isNaN(sampleGreenWeight.text)) {
316
+					tabs.setCurrentIndex(0);
317
+					displayError(TTR("manualLogEntry", "Data Entry Error"),
318
+					TTR("manualLogEntry", "Green coffee weight must be a number greater than 0."));
319
+					return false;
320
+				}
321
+			} else {
322
+				/* Production batch */
323
+				var itemArray = productionGreenTable.columnArray(0, 32).split("\\s*,\\s*");
324
+				var weightArray = productionGreenTable.columnArray(1, 0).split("\\s*,\\s*");
325
+				if((itemArray.length != weightArray.length) || (itemArray.length == 0)) {
326
+					tabs.setCurrentIndex(0);
327
+					displayError(TTR("manualLogEntry", "Data Entry Error"),
328
+					TTR("manualLogEntry", "Please check that at least one green coffee has been selected and each green coffee has a valid weight"));
329
+					return false;
330
+				}
331
+				if(Number(greenTotal) <= 0) {
332
+					tabs.setCurrentIndex(0);
333
+					displayError(TTR("manualLogEntry", "DataEntryError"),
334
+					TTR("manualLogEntry", "Total green coffee weight must be a number greater than 0."));
335
+					return false;
336
+				}
337
+				if(roastedItem.currentIndex == 0) {
338
+					tabs.setCurrentIndex(0);
339
+					displayError(TTR("manualLogEntry", "DataEntryError"),
340
+					TTR("manualLogEntry", "Please select a roasted coffee item."));
341
+					return false;
342
+				}
343
+			}
344
+			return true;
345
+		};
346
+		var roastDataExists = function() {
347
+			return (pluginContext.table.rowCount() > 0);
348
+		}
349
+		var roastTime = findChildObject(this, 'roastTime');
350
+		var attributes = findChildObject(this, 'attributes');
351
+		var sampleGreenArrivalDate = findChildObject(this, 'sampleGreenArrivalDate');
352
+		var convertToPounds = function(w, u) {
353
+			switch(u) {
354
+				case "g":
355
+					return w * 0.0022;
356
+				case "oz":
357
+					return w * 0.0625;
358
+				case "Kg":
359
+					return w * 2.2;
360
+			}
361
+			return w;
362
+		};
363
+		var roastedWeight = findChildObject(this, 'roastedWeight');
364
+		var notes = findChildObject(this, 'notes');
365
+		var roastDuration = findChildObject(this, 'roastDuration');
366
+		var doSubmit = function() {
367
+			var fileID = -1;
368
+			var query = new QSqlQuery();
369
+			if(roastDataExists()) {
370
+				var buffer = new QBuffer;
371
+				buffer.open(3);
372
+				pluginContext.table.saveXML(buffer);
373
+				buffer.open(3); /* saveXML closes the buffer */
374
+				var q = "INSERT INTO files (id, name, type, note, file) VALUES (default, :name, 'profile', NULL, :data) RETURNING id";
375
+				query.prepare(q);
376
+				query.bind(":name", roastTime.text + " Manual Entry");
377
+				query.bind(":data", buffer.readToString());
378
+				query.exec();
379
+				query.next();
380
+				fileID = query.value(0);
381
+				buffer.close();
382
+			}
383
+			var rootIndex = machineModel.index(machineSelector.currentIndex, 0);
384
+			var selectedRoasterName = machineModel.data(rootIndex, 0);
385
+			var machineReference = machineModel.referenceElement(machineModel.data(rootIndex, 32));
386
+			var selectedRoasterID = machineReference.databaseid;
387
+			query.exec("SELECT 1 FROM machine WHERE id = " + selectedRoasterID);
388
+			if(!query.next()) {
389
+				query.prepare("INSERT INTO machine (id, name) VALUES (:id, :name)");
390
+				query.bind(":id", selectedRoasterID);
391
+				query.bind(":name", selectedRoasterName);
392
+				query.exec();
393
+			}
394
+			if(batchType.currentIndex == 0) {
395
+				/* Sample roast */
396
+				var attnames = sqlToArray(attributes.columnArray(0, 0));
397
+				for(var i = 0; i < attnames.length; i++) {
398
+					var attname = attnames[i];
399
+					if(attname[0] == '{') {
400
+						attname = attname.substr(1);
401
+					}
402
+					if(attname[0] == ' ') {
403
+						attname = attname.substr(1);
404
+					}
405
+					if(attname[attname.length - 1] == '}') {
406
+						attname = attname.substr(0, attname.length - 1);
407
+					}
408
+					if(attname.length == 0) {
409
+						break;
410
+					}
411
+					query.prepare("SELECT id FROM item_attributes WHERE name = :name");
412
+					query.bind(":name", attname);
413
+					query.exec();
414
+					if(query.next()) {
415
+						attributes.setData(i, 0, query.value(0), 32);
416
+					} else {
417
+						query.prepare("INSERT INTO item_attributes(id, name) VALUES (DEFAULT, :name) RETURNING id");
418
+						query.bind(":name", attname);
419
+						query.exec();
420
+						query.next();
421
+						attributes.setData(i, 0, query.value(0), 32);
422
+					}
423
+				}
424
+				query.prepare("INSERT INTO coffee_sample_items(id, name, reference, unit, quantity, category, arrival, vendor, attribute_ids, attribute_values) VALUES (DEFAULT, :name, NULL, 'lb', 0, 'Coffee: Green Sample', :arrival, :vendor, :attrids, :attrvals) RETURNING id");
425
+				query.bind(":name", sampleGreenName.text);
426
+				query.bind(":arrival", sampleGreenArrivalDate.date);
427
+				query.bind(":attrids", attributes.bindableColumnArray(0, 32));
428
+				query.bind(":attrvals", attributes.bindableQuotedColumnArray(1, 0));
429
+				query.exec();
430
+				query.next();
431
+				var greenId = query.value(0);
432
+				query.prepare("INSERT INTO items (id, name, reference, unit, quantity, category) VALUES (DEFAULT, :name, NULL, 'lb', 0, 'Coffee: Roasted Sample') RETURNING id");
433
+				query.bind(":name", sampleGreenName.text + " Roasted Sample");
434
+				query.exec();
435
+				query.next();
436
+				var roastedId = query.value(0);
437
+				query.prepare("INSERT INTO roasting_log (time, unroasted_id, unroasted_quantity, unroasted_total_quantity, roasted_id, roasted_quantity, transaction_type, annotation, machine, duration, approval, humidity, barometric, indoor_air, outdoor_air, files, person) VALUES (:time, :unroastedids, :greens, :green, :roastedid, :roasted, 'SAMPLEROAST', :note, :machine, :duration, TRUE, NULL, NULL, NULL, NULL, :files, :user)");
438
+				query.bind(":time", roastTime.text);
439
+				query.bind(":unroastedids", "{" + greenId + "}");
440
+				query.bind(":greens", "{" + convertToPounds(parseFloat(sampleGreenWeight.text), sampleGreenUnit.currentText) + "}");
441
+				query.bind(":green", convertToPounds(parseFloat(sampleGreenWeight.text), sampleGreenUnit.currentText));
442
+				query.bind("roastedid", Number(roastedId));
443
+				query.bind("roasted", convertToPounds(parseFloat(roastedWeight.text), sampleGreenUnit.currentText));
444
+				query.bind(":note", notes.plainText);
445
+				query.bind(":machine", Number(selectedRoasterID));
446
+				query.bind(":duration", roastDuration.text);
447
+				if(fileID > 0) {
448
+					query.bind(":files", "{" + fileID + "}");
449
+				} else {
450
+					query.bind(":file", "{}");
451
+				}
452
+				query.bind(":user", Application.currentTypicaUser());
453
+				query.exec();
454
+			} else {
455
+				var q = "INSERT INTO roasting_log (time, unroasted_id, unroasted_quantity, unroasted_total_quantity, roasted_id, roasted_quantity, transaction_type, annotation, machine, duration, approval, humidity, barometric, indoor_air, outdoor_air, files, person) VALUES (:time, ";
456
+				q += productionGreenTable.columnArray(0, 32);
457
+				q += ", ";
458
+				var greenSum = 0.0;
459
+				for(var i = 0; i < productionGreenTable.data(i, 1, 0).value != ""; i++) {
460
+					var greenWt = convertToPounds(parseFloat(productionGreenTable.data(i, 1, 0)), productionGreenUnit.currentText);
461
+					productionGreenTable.setData(i, 1, greenWt, 32);
462
+					greenSum += greenWt;
463
+				}
464
+				q += productionGreenTable.columnArray(1, 32);
465
+				q += ", ";
466
+				q += greenWt;
467
+				q += ", ";
468
+				q += roastedItem.currentData();
469
+				q += ", ";
470
+				q += convertToPounds(parseFloat(roastedWeight.text), productionGreenUnit.currentText);
471
+				q += ", 'ROAST', :annotation, ";
472
+				q += selectedRoasterID;
473
+				q += ", :duration, TRUE, NULL, NULL, NULL, NULL, '{";
474
+				if(fileID > 0) {
475
+					q += fileID;
476
+				}
477
+				q += "}', :user)";
478
+				query.prepare(q);
479
+				query.bind(":time", roastTime.text);
480
+				query.bind(":annotation", notes.plainText);
481
+				query.bind(":duration", roastDuration.text);
482
+				query.bind(":user", Application.currentTypicaUser());
483
+				query.exec();
484
+			}
485
+			query = query.invalidate();
486
+			window.close();
487
+		}
488
+		var submit = findChildObject(this, 'submit');
489
+		submit.clicked.connect(function() {
490
+			if(validateInputs()) {
491
+				doSubmit();
492
+			}
493
+		});
494
+	]]>
495
+	</program>
496
+</window>

+ 125
- 65
config/Windows/navigation.xml View File

@@ -13,6 +13,11 @@
13 13
                 <button name="Roast Coffee" id="roast" type="push" />
14 14
             </column>
15 15
         </row>
16
+		<row>
17
+			<column>
18
+				<button name="Manual Roasting Log Entry" id="manual" type="push" />
19
+			</column>
20
+		</row>
16 21
         <row>
17 22
             <column>
18 23
                 <button name="Purchase Green Coffee" id="green" type="push" />
@@ -68,6 +73,10 @@
68 73
     <menu name="Database">
69 74
         <item id="resetconnection">Forget Connection Details</item>
70 75
     </menu>
76
+	<menu name="Users">
77
+		<item id="switchuser">Switch User</item>
78
+		<item id="createuser">Create New Users</item>
79
+	</menu>
71 80
     <program>
72 81
         var window = this;
73 82
         var navigationwindow = window;
@@ -87,6 +96,10 @@
87 96
             QSettings.setValue("database/user", "");
88 97
             QSettings.setValue("database/password", "");
89 98
         });
99
+		var manual = findChildObject(this, 'manual');
100
+		manual.clicked.connect(function() {
101
+			createWindow("manualLogEntry");
102
+		});
90 103
         var profilehistory = findChildObject(this, 'profilehistory');
91 104
         profilehistory.clicked.connect(function() {
92 105
                 createWindow("profilehistory");
@@ -133,7 +146,7 @@
133 146
         var nrbutton = findChildObject(this, 'newroasted');
134 147
         nrbutton.clicked.connect(function() {
135 148
             var nrwindow = createWindow("newroasted");
136
-            nrwindow.windowTitle = "New Roasted Coffee Item";
149
+            nrwindow.windowTitle = "Manage Roasted Coffee Items";
137 150
         });
138 151
         var importb = findChildObject(this, 'target');
139 152
         importb.clicked.connect(function() {
@@ -226,7 +239,7 @@
226 239
 		   Typica 1.6 and later. */
227 240
 		var DBCreateSampleRoasting = function() {
228 241
 			var query = new QSqlQuery;
229
-			query.exec("CREATE TABLE IF NOT EXISTS item_attributes (id bigint PRIMARY KEY NOT NULL, name text NOT NULL)");
242
+			query.exec("CREATE TABLE IF NOT EXISTS item_attributes (id bigserial PRIMARY KEY NOT NULL, name text NOT NULL)");
230 243
 			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)");
231 244
 			query.exec("INSERT INTO TypicaFeatures (feature, enabled, version) VALUES('sample-roasting', TRUE, 1)");
232 245
 			query = query.invalidate();
@@ -272,77 +285,124 @@
272 285
 		/* Update trigger functions to make column names explicit */
273 286
 		var DBUpdateTriggers = function() {
274 287
 			var query = new QSqlQuery;
275
-			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) VALUES(NEW.time, NEW.roasted_id, NEW.roasted_quantity); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql");
276 288
 			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");
277
-			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) VALUES(NEW.time, NEW.unroasted_id[i], NEW.unroasted_quantity[i]); i := i + 1; END LOOP; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql");
278 289
 			query.exec("UPDATE TypicaFeatures SET version = 4 WHERE feature = 'base-features'");
279 290
 		};
280
-                var DBUpdateReminders = function() {
281
-                    var query = new QSqlQuery;
282
-                    query.exec("CREATE TABLE IF NOT EXISTS reminders (id bigserial PRIMARY KEY NOT NULL, reminder text NOT NULL)");
283
-                    query.exec("UPDATE TypicaFeatures SET version = 5 WHERE feature = 'base-features'");
284
-                    query = query.invalidate();
285
-                };
291
+        var DBUpdateReminders = function() {
292
+            var query = new QSqlQuery;
293
+            query.exec("CREATE TABLE IF NOT EXISTS reminders (id bigserial PRIMARY KEY NOT NULL, reminder text NOT NULL)");
294
+            query.exec("UPDATE TypicaFeatures SET version = 5 WHERE feature = 'base-features'");
295
+            query = query.invalidate();
296
+        };
286 297
 		var DBUpdateSpecification = function() {
287
-                    var query = new QSqlQuery;
288
-                    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)");
289
-                    query.exec("UPDATE TypicaFeatures SET version = 6 WHERE feature = 'base-features'");
290
-                    query = query.invalidate();
291
-                };
292
-                
293
-		query = new QSqlQuery();
294
-		/* A table keeps track of database versioning information. This table is created
295
-		   if required. */
296
-		query.exec("CREATE TABLE IF NOT EXISTS TypicaFeatures (feature TEXT PRIMARY KEY, enabled boolean, version bigint)");
297
-		/* At the moment everything we're interested in is covered in the base-features
298
-		   row, but this can be extended later if needed. Each row encodes if certain
299
-		database structures exist and what version of those structures exist. */
300
-		query.exec("SELECT feature, enabled, version FROM TypicaFeatures WHERE feature = 'base-features'");
301
-		if(query.next())
302
-		{
303
-                    if(query.value(2) < 1)
304
-                    {
305
-                            DBCreateBase();
306
-                    }
307
-                    if(query.value(2) < 2)
308
-                    {
309
-                            DBUpdateMultiUser();
310
-                            DBUpdateHistory();
311
-                    }
312
-                    if(query.value(2) < 3)
313
-                    {
314
-                            DBUpdateNotifications();
315
-                    }
316
-                    if(query.value(2) < 4)
317
-                    {
318
-                            DBUpdateTriggers();
319
-                    }
320
-                    if(query.value(2) < 5)
321
-                    {
322
-                        DBUpdateReminders();
323
-                    }
324
-                    if(query.value(2) < 6)
325
-                    {
326
-                        DBUpdateSpecification();
327
-                    }
328
-		}
329
-		else
330
-		{
331
-			DBCreateBase();
332
-		}
333
-		query.exec("SELECT feature, enabled, version FROM TypicaFeatures WHERE feature = 'sample-roasting'");
334
-		if(query.next())
335
-		{
336
-			if(query.value(2) < 1)
298
+            var query = new QSqlQuery;
299
+            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)");
300
+            query.exec("UPDATE TypicaFeatures SET version = 6 WHERE feature = 'base-features'");
301
+            query = query.invalidate();
302
+        };
303
+		
304
+		/* Updates for Typica version 1.8 */
305
+		var DBUpdate18 = function() {
306
+			var query = new QSqlQuery;
307
+			/* Create a table for Typica users login data */
308
+			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)");
309
+			/* 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. */
310
+			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");
311
+			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");
312
+			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");
313
+			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)");
314
+			query.exec("UPDATE TypicaFeatures SET version = 7 WHERE feature = 'base-features'");
315
+			query = query.invalidate();
316
+		};
317
+		if(Application.databaseConnected()) {
318
+			query = new QSqlQuery();
319
+			/* A table keeps track of database versioning information. This
320
+			   table is created
321
+			   if required. */
322
+			query.exec("CREATE TABLE IF NOT EXISTS TypicaFeatures (feature TEXT PRIMARY KEY, enabled boolean, version bigint)");
323
+			query.exec("SELECT feature, enabled, version FROM TypicaFeatures WHERE feature = 'base-features'");
324
+			if(query.next())
325
+			{
326
+				if(query.value(2) < 1)
327
+				{
328
+				        DBCreateBase();
329
+				}
330
+				if(query.value(2) < 2)
331
+				{
332
+			        DBUpdateMultiUser();
333
+			        DBUpdateHistory();
334
+				}
335
+				if(query.value(2) < 3)
336
+				{
337
+			        DBUpdateNotifications();
338
+				}
339
+				if(query.value(2) < 4)
340
+				{
341
+			        DBUpdateTriggers();
342
+				}
343
+				if(query.value(2) < 5)
344
+				{
345
+				    DBUpdateReminders();
346
+				}
347
+				if(query.value(2) < 6)
348
+				{
349
+					DBUpdateSpecification();
350
+				}
351
+				if(query.value(2) < 7) {
352
+					DBUpdate18();
353
+				}
354
+			}
355
+			else
356
+			{
357
+				DBCreateBase();
358
+				DBUpdateMultiUser();
359
+		        DBUpdateHistory();
360
+				DBUpdateNotifications();
361
+				DBUpdateTriggers();
362
+				DBUpdateReminders();
363
+				DBUpdateSpecification();
364
+				DBUpdate18();
365
+			}
366
+			query.exec("SELECT feature, enabled, version FROM TypicaFeatures WHERE feature = 'sample-roasting'");
367
+			if(query.next())
368
+			{
369
+				if(query.value(2) < 1)
370
+				{
371
+					DBCreateSampleRoasting();
372
+				}
373
+			}
374
+			else
337 375
 			{
338 376
 				DBCreateSampleRoasting();
339 377
 			}
378
+			var promptNewUsers = true;
379
+			query.exec("SELECT count(1) FROM typica_users");
380
+			if(query.next()) {
381
+				if(Number(query.value(0)) > 0) {
382
+					promptNewUsers = false;
383
+				}
384
+			}
385
+			if(promptNewUsers) {
386
+				var newUserDialog = new NewTypicaUser();
387
+				newUserDialog.exec();
388
+			}
389
+			if(!Application.autoLogin()) {
390
+				var loginDialog = new LoginDialog();
391
+				loginDialog.exec();
392
+			}
393
+			query = query.invalidate();
340 394
 		}
341
-		else
342
-		{
343
-			DBCreateSampleRoasting();
344
-		}
345
-		query = query.invalidate();
395
+		
396
+		var switchuser = findChildObject(this, 'switchuser');
397
+		switchuser.triggered.connect(function() {
398
+			var loginDialog = new LoginDialog();
399
+			loginDialog.exec();
400
+		});
401
+		var createuser = findChildObject(this, 'createuser');
402
+		createuser.triggered.connect(function() {
403
+			var newUserDialog = new NewTypicaUser();
404
+			newUserDialog.exec();
405
+		});
346 406
         ]]>
347 407
     </program>
348 408
 </window>

+ 124
- 15
config/Windows/newbatch.xml View File

@@ -1,4 +1,7 @@
1 1
 <window id="batchWindow">
2
+	<menu name="File">
3
+		<item id="print" shortcut="Ctrl+P">Print...</item>
4
+	</menu>
2 5
     <menu name="Batch">
3 6
         <item id="new" shortcut="Ctrl+N">New Batch...</item>
4 7
     </menu>
@@ -75,8 +78,19 @@
75 78
             </layout>
76 79
             <label>Specification Details</label>
77 80
             <textarea id="specnotes" />
81
+            <layout type="horizontal">
82
+                <label>File ID:</label>
83
+                <line id="filenofield" writable="false" />
84
+            </layout>
78 85
             <stretch />
79 86
         </layout>
87
+		<layout type="vertical">
88
+			<webview id="batchTag" />
89
+			<layout type="horizontal">
90
+				<printerselector id="printerlist" />
91
+				<button name="Print" id="printbutton" type="push" />
92
+			</layout>
93
+		</layout>
80 94
     </layout>
81 95
     <program>
82 96
         <![CDATA[
@@ -107,6 +121,7 @@
107 121
             roastwt.maximumWidth = 80;
108 122
             var scalesLayout = findChildObject(this, 'scales');
109 123
             scalesLayout.spacing = 10;
124
+			var batchTag = findChildObject(this, 'batchTag');
110 125
             if(navigationwindow.loggingWindow.scales.length > 0) {
111 126
                 for(var i = 0; i < navigationwindow.loggingWindow.scales.length; i++) {
112 127
                     var scale = navigationwindow.loggingWindow.scales[i];
@@ -378,8 +393,8 @@
378 393
                     lossspec.text = "";
379 394
                     specnotes.plainText = "";
380 395
                 }
381
-                roastestimate.text = "";
382 396
                 query = query.invalidate();
397
+				drawTag();
383 398
             });
384 399
             var validateCapacity = function() {
385 400
                 if(checkCapacity == "true") {
@@ -389,6 +404,8 @@
389 404
                 }
390 405
                 return true;
391 406
             }
407
+            var duration = findChildObject(this, 'duration');
408
+            var timefield = findChildObject(this, 'time');
392 409
             profilebutton.clicked.connect(function() {
393 410
                 var proceed = false;
394 411
                 if(validateCapacity()) {
@@ -397,6 +414,10 @@
397 414
                     proceed = displayWarning(TTR("batchWindow", "Suspicious Input"),
398 415
                     TTR("batchWindow", "Entered green coffee weight exceeds maximum batch size. Continue?"));
399 416
                 }
417
+                if((proceed == true) && (timefield.text.length != 0)) {
418
+                    proceed = displayWarning(TTR("batchWindow", "Batch Already Roasted"),
419
+                    TTR("batchWindow", "Roasting data already exists for this batch. Roasting another batch from this window will overwrite existing data. Are you sure you want to do this?"));
420
+                }
400 421
                 if(proceed) {
401 422
                     doLoadProfile();
402 423
                 }
@@ -480,6 +501,10 @@
480 501
                     proceed = displayWarning(TTR("batchWindow", "Suspicious Input"),
481 502
                     TTR("batchWindow", "Entered green coffee weight exceeds maximum batch size. Continue?"));
482 503
                 }
504
+                if((proceed == true) && (timefield.text.length != 0)) {
505
+                    proceed = displayWarning(TTR("batchWindow", "Batch Already Roasted"),
506
+                    TTR("batchWindow", "Roasting data already exists for this batch. Roasting another batch from this window will overwrite existing data. Are you sure you want to do this?"));
507
+                }
483 508
                 if(proceed) {
484 509
                     doNoProfile();
485 510
                 }
@@ -491,19 +516,24 @@
491 516
                 navigationwindow.loggingWindow.activateWindow();
492 517
             }
493 518
             var submitbutton = findChildObject(this, 'submit');
494
-            var timefield = findChildObject(this, 'time');
495 519
             var notes = findChildObject(this, 'annotation');
496
-            var duration = findChildObject(this, 'duration');
497 520
             var approval = findChildObject(this, 'approval');
498 521
             approval.checked = true;
499 522
             var target = findChildObject(this, 'target');
523
+            var greenCheck = function() {
524
+                var itemArray = table.columnArray(0, 32).split("\\s*,\\s*");
525
+                var weightArray = table.columnArray(1, 0).split("\\s*,\\s*");
526
+                return (itemArray.length == weightArray.length) && (itemArray.length > 0);
527
+            }
500 528
             var checkSubmitEnable = function () {
501 529
                 if(roasted.currentIndex > 0) {
502 530
                     if(timefield.text.length > 0) {
503 531
                         if(duration.text.length > 0) {
504 532
                             if(batch.tempData.length > 0) {
505 533
                                 if(green.text.length > 0) {
506
-                                    return true;
534
+                                    if(greenCheck()) {
535
+                                        return true;
536
+                                    }
507 537
                                 }
508 538
                             }
509 539
                         }
@@ -528,6 +558,20 @@
528 558
                     }
529 559
                 }
530 560
             });
561
+            var filenofield = findChildObject(this, 'filenofield');
562
+            this.endBatch = function() {
563
+                var q = "INSERT INTO files (id, name, type, note, file) VALUES(default, :name, 'profile', NULL, :data) RETURNING id";
564
+                var query = new QSqlQuery();
565
+                query.prepare(q);
566
+                query.bind(":name", timefield.text + " " + roasted.currentText);
567
+                query.bindFileData(":data", batch.tempData);
568
+                query.exec();
569
+                query.next();
570
+                filenofield.text = query.value(0);
571
+                var file = new QFile(batch.tempData);
572
+                file.remove();
573
+				drawTag();
574
+            }
531 575
             var doSubmit = function() {
532 576
                 checkQuery = new QSqlQuery();
533 577
                 checkQuery.exec("SELECT 1 FROM machine WHERE id = " + selectedRoasterID);
@@ -539,17 +583,9 @@
539 583
                     checkQuery.exec();
540 584
                 }
541 585
                 checkQuery = checkQuery.invalidate();
542
-                var q = "INSERT INTO files (id, name, type, note, file) VALUES(default, :name, 'profile', NULL, :data) RETURNING id";
543 586
                 query = new QSqlQuery();
544
-                query.prepare(q);
545
-                query.bind(":name", timefield.text + " " + roasted.currentText);
546
-                query.bindFileData(":data", batch.tempData);
547
-                query.exec();
548
-                query.next();
549
-                var fileno = query.value(0);
550
-                var file = new QFile(batch.tempData);
551
-                file.remove();
552
-                var q2 = "INSERT INTO roasting_log (time, unroasted_id, unroasted_quantity, unroasted_total_quantity, roasted_id, roasted_quantity, transaction_type, annotation, machine, duration, approval, humidity, barometric, indoor_air, outdoor_air, files) VALUES(:time, ";
587
+                var fileno = Number(filenofield.text);
588
+                var q2 = "INSERT INTO roasting_log (time, unroasted_id, unroasted_quantity, unroasted_total_quantity, roasted_id, roasted_quantity, transaction_type, annotation, machine, duration, approval, humidity, barometric, indoor_air, outdoor_air, files, person) VALUES(:time, ";
553 589
                 q2 = q2 + table.columnArray(0, 32);
554 590
                 q2 = q2 + ", ";
555 591
                 for(var i = 0; table.data(i, 1, 0).value != ""; i++)
@@ -567,14 +603,21 @@
567 603
                 q2 = q2 + selectedRoasterID;
568 604
                 q2 = q2 + ", :duration, :approval, NULL, NULL, NULL, NULL, '{";
569 605
                 q2 = q2 + fileno;
570
-                q2 = q2 + "}')";
606
+                q2 = q2 + "}', :user) RETURNING time";
571 607
                 query2 = new QSqlQuery();
572 608
                 query2.prepare(q2);
573 609
                 query2.bind(":time", timefield.text);
574 610
                 query2.bind(":annotation", notes.plainText);
575 611
                 query2.bind(":duration", duration.text);
576 612
                 query2.bind(":approval", approval.checked);
613
+				query2.bind(":user", Application.currentTypicaUser());
577 614
                 query2.exec();
615
+                if(!query2.next()) {
616
+                    displayError(TTR("batchWindow", "Database Insert Failed"), TTR("batchWindow", "Failed to save batch to database. Please check inputs and connection and try again."));
617
+                    query2 = query2.invalidate();
618
+                    query = query.invalidate();
619
+                    return;
620
+                }
578 621
                 query2 = query2.invalidate();
579 622
                 if(target.checked) {
580 623
                     var q3 = "INSERT INTO item_files (time, item, files) VALUES(:time, :item, '{";
@@ -589,6 +632,72 @@
589 632
                 batch.windowModified = false;
590 633
                 batch.close();
591 634
             }
635
+			function drawTag() {
636
+				var buffer = new QBuffer;
637
+                buffer.open(3);
638
+                var output = new XmlWriter(buffer);
639
+                output.writeStartDocument("1.0");
640
+                output.writeDTD('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg.dtd">');
641
+                output.writeStartElement("html");
642
+                output.writeAttribute("xmlns", "http://www.w3.org/1999/xhtml");
643
+				output.writeStartElement("head");
644
+				var styleFile = new QFile(QSettings.value("config") + "/Scripts/batchtag.css");
645
+				styleFile.open(1);
646
+				output.writeTextElement("style", styleFile.readToString());
647
+				styleFile.close();
648
+				output.writeStartElement("script");
649
+				scriptFile = new QFile(QSettings.value("config") + "/Scripts/qrcode.js");
650
+				scriptFile.open(1);
651
+				output.writeCDATA(scriptFile.readToString());
652
+				scriptFile.close();
653
+				output.writeEndElement();
654
+				output.writeEndElement();
655
+                output.writeStartElement("body");
656
+				output.writeStartElement("h1");
657
+				output.writeCharacters(roasted.currentText);
658
+				output.writeEndElement();
659
+				output.writeTextElement("span", "Roasted at: " + timefield.text);
660
+				output.writeTextElement("span", "On machine: " + machine.text);
661
+				output.writeTextElement("span", "Batch file: " + filenofield.text);
662
+				output.writeStartElement("div");
663
+				output.writeAttribute("id", "container");
664
+				output.writeEndElement();
665
+				output.writeStartElement("script");
666
+				var tag = {g: "Typica", m: Number(selectedRoasterID), v: 1};
667
+				if(timefield.text.length > 0) {
668
+					tag.t = timefield.text;
669
+				}
670
+				if(filenofield.text.length > 0) {
671
+					tag.f = Number(filenofield.text);
672
+				}
673
+				var scriptData = 'var width = document.getElementById("container").offsetWidth;';
674
+				scriptData += 'var qrcode = new QRCode({content: \'';
675
+				scriptData += JSON.stringify(tag);
676
+				scriptData += '\', width: width, height: width});';
677
+				scriptData += 'var svg = qrcode.svg();';
678
+				scriptData += 'document.getElementById("container").innerHTML = svg;';
679
+				output.writeCDATA(scriptData);
680
+				output.writeEndElement();
681
+				output.writeEndElement();
682
+				output.writeEndElement();
683
+				output.writeEndDocument();
684
+                batchTag.setContent(buffer);
685
+                buffer.close();
686
+			};
687
+			drawTag();
688
+			var printMenu = findChildObject(this, 'print');
689
+            printMenu.triggered.connect(function() {
690
+                batchTag.print();
691
+            });
692
+			var printers = findChildObject(this, 'printerlist');
693
+			printers.currentIndex = printers.findText(QSettings.value("script/batchtagprinter"));
694
+			printers['currentIndexChanged(int)'].connect(function() {
695
+                QSettings.setValue("script/batchtagprinter", printers.currentText);
696
+            });
697
+			var printbutton = findChildObject(this, 'printbutton');
698
+			printbutton.clicked.connect(function() {
699
+				batchTag.print(printers.currentText);
700
+			});
592 701
         ]]>
593 702
     </program>
594 703
 </window>

+ 35
- 33
config/Windows/newsamplebatch.xml View File

@@ -132,6 +132,7 @@
132 132
 			newMenu.triggered.connect(function() {
133 133
 				createWindow("sampleRoastingBatch");
134 134
 			});
135
+            this.endBatch = function() {};
135 136
 			var batch = this;
136 137
 			batch.submitButton = submit;
137 138
 			var name = findChildObject(this, 'name');
@@ -179,27 +180,27 @@
179 180
 			stop.clicked.connect(function() {
180 181
 				submit.setEnabled(true);
181 182
 			});
182
-                        var validateCapacity = function() {
183
-                            if(checkCapacity == "true") {
184
-                                if(convertToPounds(parseFloat(green.text), GunitBox.currentText) > poundsCapacity) {
185
-                                    return false;
186
-                                }
187
-                            }
188
-                            return true;
189
-                        }
190
-                        roastButton.clicked.connect(function() {
191
-                            var proceed = false;
192
-                            if(validateCapacity()) {
193
-                                proceed = true;
194
-                            } else {
195
-                                proceed = displayWarning(TTR("sampleRoastingBatch", "Suspicious Input"),
183
+            var validateCapacity = function() {
184
+                if(checkCapacity == "true") {
185
+                    if(convertToPounds(parseFloat(green.text), GunitBox.currentText) > poundsCapacity) {
186
+                        return false;
187
+                    }
188
+                }
189
+                return true;
190
+            }
191
+            roastButton.clicked.connect(function() {
192
+                var proceed = false;
193
+                if(validateCapacity()) {
194
+                    proceed = true;
195
+                } else {
196
+                    proceed = displayWarning(TTR("sampleRoastingBatch", "Suspicious Input"),
196 197
                                 TTR("sampleRoastingBatch", "Entered green coffee weight exceeds maximum batch size. Continue?"));
197
-                            }
198
-                            if(proceed) {
199
-                                doRoast();
200
-                            }
201
-                        });
202
-                        var doRoast = function() {
198
+                }
199
+                if(proceed) {
200
+                    doRoast();
201
+                }
202
+                });
203
+                var doRoast = function() {
203 204
 				var lc = 1;
204 205
 				currentBatchInfo = batch;
205 206
 				query = new QSqlQuery();
@@ -270,19 +271,19 @@
270 271
 			var vendor = findChildObject(this, 'vendor');
271 272
 			var attributes = findChildObject(this, 'attributes');
272 273
 			var target = findChildObject(this, 'target');
273
-                        submit.clicked.connect(function() {
274
-                            var proceed = false;
275
-                            if(validateCapacity()) {
276
-                                proceed = true;
277
-                            } else {
278
-                                proceed = displayWarning(TTR("sampleRoastingBatch", "Suspicious Input"),
274
+            submit.clicked.connect(function() {
275
+                var proceed = false;
276
+                if(validateCapacity()) {
277
+                    proceed = true;
278
+                } else {
279
+                    proceed = displayWarning(TTR("sampleRoastingBatch", "Suspicious Input"),
279 280
                                 TTR("sampleRoastingBatch", "Entered green coffee weight exceeds maximum batch size. Continue?"));
280
-                            }
281
-                            if(proceed) {
282
-                                doLoadProfile();
283
-                            }
284
-                        });
285
-                        var doSubmit = function() {
281
+                }
282
+                if(proceed) {
283
+                    doSubmit();
284
+                }
285
+            });
286
+            var doSubmit = function() {
286 287
 				query = new QSqlQuery();
287 288
 				query.prepare("INSERT INTO files (id, name, type, note, file) VALUES(DEFAULT, :name, 'profile', NULL, :data) RETURNING id");
288 289
 				query.bind(":name", timefield.text + " " + name.text + " " + profileName.currentText);
@@ -346,7 +347,7 @@
346 347
 				query.exec();
347 348
 				query.next();
348 349
 				var roastedId = query.value(0);
349
-				query.prepare("INSERT INTO roasting_log (time, unroasted_id, unroasted_quantity, unroasted_total_quantity, roasted_id, roasted_quantity, transaction_type, annotation, machine, duration, approval, humidity, barometric, indoor_air, outdoor_air, files) VALUES(:time, :unroastedids, :greens, :green, :roastedid, :roasted, 'SAMPLEROAST', :note, :machine, :duration, TRUE, NULL, NULL, NULL, NULL, :files)");
350
+				query.prepare("INSERT INTO roasting_log (time, unroasted_id, unroasted_quantity, unroasted_total_quantity, roasted_id, roasted_quantity, transaction_type, annotation, machine, duration, approval, humidity, barometric, indoor_air, outdoor_air, files, person) VALUES(:time, :unroastedids, :greens, :green, :roastedid, :roasted, 'SAMPLEROAST', :note, :machine, :duration, TRUE, NULL, NULL, NULL, NULL, :files, :user)");
350 351
 				query.bind(":time", timefield.text);
351 352
 				query.bind(":unroastedids", "{" + greenId + "}");
352 353
                                 query.bind(":greens", "{" + convertToPounds(parseFloat(green.text), GunitBox.currentText) + "}");
@@ -357,6 +358,7 @@
357 358
 				query.bind(":machine", Number(selectedRoasterID));
358 359
 				query.bind(":duration", duration.text);
359 360
 				query.bind(":files", "{" + fileno + "}");
361
+				query.bind(":user", Application.currentTypicaUser());
360 362
 				query.exec();
361 363
 				query = query.invalidate();
362 364
 				batch.close();

+ 870
- 767
config/Windows/productionroaster.xml
File diff suppressed because it is too large
View File


+ 2
- 1
config/Windows/purchase.xml View File

@@ -317,7 +317,7 @@
317 317
                     query.exec();
318 318
                     query.next();
319 319
                     var item_id = query.value(0);
320
-                    q = "INSERT INTO purchase (time, item, quantity, cost, vendor) VALUES(:time, :item, :quantity, :cost, :vendor)";
320
+                    q = "INSERT INTO purchase (time, item, quantity, cost, vendor, person) VALUES(:time, :item, :quantity, :cost, :vendor, :user)";
321 321
                     query.prepare(q);
322 322
                     query.bind(":time", dateField.date);
323 323
                     query.bind(":item", item_id);
@@ -332,6 +332,7 @@
332 332
                         query.bind(":cost", Number(costEntry.text) / convertToPounds(parseFloat(quantityEntry.text), unitEntry.currentText));
333 333
                     }
334 334
                     query.bind(":vendor", vendorField.currentText);
335
+					query.bind(":user", Application.currentTypicaUser());
335 336
                     query.exec();
336 337
                     q = "INSERT INTO lb_bag_conversion (item, conversion) VALUES(:item, :conversion)";
337 338
                     query.prepare(q);

+ 1
- 0
config/config.xml View File

@@ -33,6 +33,7 @@
33 33
     <include src="Windows/newsamplebatch.xml" />
34 34
     <include src="Windows/editreminder.xml" />
35 35
     <include src="Windows/roastspec.xml" />
36
+	<include src="Windows/manuallogentry.xml" />
36 37
     <program>
37 38
         <![CDATA[
38 39
             Windows = new Object();

BIN
docs/documentation/windowreference/manageroasted.png View File


+ 1
- 1
src/3rdparty/qextserialport/src/qextserialenumerator_linux.cpp View File

@@ -135,7 +135,7 @@ QList<QextPortInfo> QextSerialEnumeratorPrivate::getPorts_sys()
135 135
     // (USB-serial, bluetooth-serial, 18F PICs, and so on)
136 136
     // if you know an other name prefix for serial ports please let us know
137 137
     portNamePrefixes.clear();
138
-    portNamePrefixes << QLatin1String("ttyACM*") << QLatin1String("ttyUSB*") << QLatin1String("rfcomm*");
138
+    portNamePrefixes << QLatin1String("ttyACM*") << QLatin1String("ttyUSB*") << QLatin1String("rfcomm*") << QLatin1String("tnt*");
139 139
     portNameList += dir.entryList(portNamePrefixes, (QDir::System | QDir::Files), QDir::Name);
140 140
 
141 141
     foreach (QString str , portNameList) {

+ 539
- 265
src/Translations/Typica.ts
File diff suppressed because it is too large
View File


BIN
src/Translations/Typica_de.qm View File


+ 538
- 264
src/Translations/Typica_de.ts
File diff suppressed because it is too large
View File


+ 5
- 2
src/Typica.pro View File

@@ -26,7 +26,8 @@ HEADERS += moc_typica.cpp \
26 26
     scale.h \
27 27
     draglabel.h \
28 28
     daterangeselector.h \
29
-    licensewindow.h
29
+    licensewindow.h \
30
+    printerselector.h
30 31
 SOURCES += typica.cpp \
31 32
     helpmenu.cpp \
32 33
     abouttypica.cpp \
@@ -36,7 +37,8 @@ SOURCES += typica.cpp \
36 37
     scale.cpp \
37 38
     draglabel.cpp \
38 39
     daterangeselector.cpp \
39
-    licensewindow.cpp
40
+    licensewindow.cpp \
41
+    printerselector.cpp
40 42
 
41 43
 RESOURCES += \
42 44
     resources.qrc
@@ -46,3 +48,4 @@ ICON = resources/icons/appicons/logo.icns
46 48
 QMAKE_INFO_PLIST = resources/Info.plist
47 49
 
48 50
 CODECFORTR = UTF-8
51
+TRANSLATIONS = Translations/Typica_de.ts

+ 5
- 5
src/abouttypica.cpp View File

@@ -1,9 +1,9 @@
1
-/*275:*/
1
+/*287:*/
2 2
 #line 33 "./abouttypica.w"
3 3
 
4 4
 #include "abouttypica.h"
5 5
 
6
-/*276:*/
6
+/*288:*/
7 7
 #line 42 "./abouttypica.w"
8 8
 
9 9
 AboutTypica::AboutTypica():QMainWindow(NULL)
@@ -17,10 +17,10 @@ aboutFile.close();
17 17
 setCentralWidget(banner);
18 18
 }
19 19
 
20
-#line 6592 "./typica.w"
20
+#line 6808 "./typica.w"
21 21
 
22
-/*:276*/
22
+/*:288*/
23 23
 #line 36 "./abouttypica.w"
24 24
 
25 25
 
26
-/*:275*/
26
+/*:287*/

+ 2
- 2
src/abouttypica.h View File

@@ -1,4 +1,4 @@
1
-/*274:*/
1
+/*286:*/
2 2
 #line 14 "./abouttypica.w"
3 3
 
4 4
 #include <QMainWindow> 
@@ -17,4 +17,4 @@ AboutTypica();
17 17
 
18 18
 #endif
19 19
 
20
-/*:274*/
20
+/*:286*/

+ 20
- 20
src/daterangeselector.cpp View File

@@ -1,4 +1,4 @@
1
-/*666:*/
1
+/*695:*/
2 2
 #line 70 "./daterangeselector.w"
3 3
 
4 4
 #include <QCalendarWidget> 
@@ -11,7 +11,7 @@
11 11
 
12 12
 #include "daterangeselector.h"
13 13
 
14
-/*668:*/
14
+/*697:*/
15 15
 #line 115 "./daterangeselector.w"
16 16
 
17 17
 CustomDateRangePopup::CustomDateRangePopup(QWidget*parent):
@@ -55,7 +55,7 @@ outerLayout->addLayout(buttonLayout);
55 55
 setLayout(outerLayout);
56 56
 }
57 57
 
58
-/*:668*//*669:*/
58
+/*:697*//*698:*/
59 59
 #line 163 "./daterangeselector.w"
60 60
 
61 61
 void CustomDateRangePopup::hideEvent(QHideEvent*)
@@ -63,7 +63,7 @@ void CustomDateRangePopup::hideEvent(QHideEvent*)
63 63
 emit hidingPopup();
64 64
 }
65 65
 
66
-/*:669*//*670:*/
66
+/*:698*//*699:*/
67 67
 #line 172 "./daterangeselector.w"
68 68
 
69 69
 void CustomDateRangePopup::applyRange()
@@ -78,7 +78,7 @@ endDateSelector->selectedDate().toString(Qt::ISODate)));
78 78
 hide();
79 79
 }
80 80
 
81
-/*:670*//*671:*/
81
+/*:699*//*700:*/
82 82
 #line 189 "./daterangeselector.w"
83 83
 
84 84
 void CustomDateRangePopup::validateRange()
@@ -93,10 +93,10 @@ applyButton->setEnabled(true);
93 93
 }
94 94
 }
95 95
 
96
-/*:671*/
96
+/*:700*/
97 97
 #line 81 "./daterangeselector.w"
98 98
 
99
-/*672:*/
99
+/*701:*/
100 100
 #line 207 "./daterangeselector.w"
101 101
 
102 102
 DateRangeSelector::DateRangeSelector(QWidget*parent):
@@ -108,7 +108,7 @@ connect(quickSelector,SIGNAL(currentIndexChanged(int)),this,SLOT(updateRange(int
108 108
 QDate currentDate= QDate::currentDate();
109 109
 
110 110
 QHBoxLayout*layout= new QHBoxLayout;
111
-/*673:*/
111
+/*702:*/
112 112
 #line 236 "./daterangeselector.w"
113 113
 
114 114
 quickSelector->addItem("Yesterday",QVariant(QStringList()<<
@@ -188,7 +188,7 @@ quickSelector->insertSeparator(quickSelector->count());
188 188
 quickSelector->addItem("Lifetime");
189 189
 quickSelector->addItem("Custom");
190 190
 
191
-/*:673*/
191
+/*:702*/
192 192
 #line 217 "./daterangeselector.w"
193 193
 
194 194
 QToolButton*customButton= new QToolButton;
@@ -201,7 +201,7 @@ setLayout(layout);
201 201
 connect(customButton,SIGNAL(clicked()),this,SLOT(toggleCustom()));
202 202
 }
203 203
 
204
-/*:672*//*674:*/
204
+/*:701*//*703:*/
205 205
 #line 319 "./daterangeselector.w"
206 206
 
207 207
 void DateRangeSelector::updateRange(int index)
@@ -217,7 +217,7 @@ emit rangeUpdated(quickSelector->itemData(quickSelector->currentIndex()));
217 217
 }
218 218
 }
219 219
 
220
-/*:674*//*675:*/
220
+/*:703*//*704:*/
221 221
 #line 336 "./daterangeselector.w"
222 222
 
223 223
 void DateRangeSelector::popupHidden()
@@ -227,7 +227,7 @@ customRangeSelector= NULL;
227 227
 quickSelector->setCurrentIndex(lastIndex);
228 228
 }
229 229
 
230
-/*:675*//*676:*/
230
+/*:704*//*705:*/
231 231
 #line 347 "./daterangeselector.w"
232 232
 
233 233
 void DateRangeSelector::setCustomRange(QVariant range)
@@ -238,7 +238,7 @@ lastIndex= quickSelector->count()-1;
238 238
 quickSelector->setCurrentIndex(lastIndex);
239 239
 }
240 240
 
241
-/*:676*//*677:*/
241
+/*:705*//*706:*/
242 242
 #line 362 "./daterangeselector.w"
243 243
 
244 244
 void DateRangeSelector::toggleCustom()
@@ -279,7 +279,7 @@ customRangeSelector= NULL;
279 279
 }
280 280
 }
281 281
 
282
-/*:677*//*678:*/
282
+/*:706*//*707:*/
283 283
 #line 404 "./daterangeselector.w"
284 284
 
285 285
 QVariant DateRangeSelector::currentRange()
@@ -287,7 +287,7 @@ QVariant DateRangeSelector::currentRange()
287 287
 return quickSelector->itemData(lastIndex);
288 288
 }
289 289
 
290
-/*:678*//*679:*/
290
+/*:707*//*708:*/
291 291
 #line 412 "./daterangeselector.w"
292 292
 
293 293
 void DateRangeSelector::setCurrentIndex(int index)
@@ -300,7 +300,7 @@ int DateRangeSelector::currentIndex()
300 300
 return quickSelector->currentIndex();
301 301
 }
302 302
 
303
-/*:679*//*680:*/
303
+/*:708*//*709:*/
304 304
 #line 432 "./daterangeselector.w"
305 305
 
306 306
 void DateRangeSelector::setLifetimeRange(QString startDate,QString endDate)
@@ -309,7 +309,7 @@ quickSelector->setItemData(quickSelector->count()-2,
309 309
 QVariant(QStringList()<<startDate<<endDate));
310 310
 }
311 311
 
312
-/*:680*//*681:*/
312
+/*:709*//*710:*/
313 313
 #line 442 "./daterangeselector.w"
314 314
 
315 315
 void DateRangeSelector::removeIndex(int index)
@@ -317,12 +317,12 @@ void DateRangeSelector::removeIndex(int index)
317 317
 quickSelector->removeItem(index);
318 318
 }
319 319
 
320
-/*:681*/
320
+/*:710*/
321 321
 #line 82 "./daterangeselector.w"
322 322
 
323 323
 
324
-#ifdef __unix__
324
+#if __APPLE__
325 325
 #include "moc_daterangeselector.cpp"
326 326
 #endif
327 327
 
328
-/*:666*/
328
+/*:695*/

+ 4
- 4
src/daterangeselector.h View File

@@ -1,4 +1,4 @@
1
-/*665:*/
1
+/*694:*/
2 2
 #line 30 "./daterangeselector.w"
3 3
 
4 4
 
@@ -9,7 +9,7 @@
9 9
 #ifndef TypicaDateRangeSelectorHeader
10 10
 #define TypicaDateRangeSelectorHeader
11 11
 
12
-/*667:*/
12
+/*696:*/
13 13
 #line 91 "./daterangeselector.w"
14 14
 
15 15
 class CustomDateRangePopup:public QWidget
@@ -31,7 +31,7 @@ QCalendarWidget*endDateSelector;
31 31
 QPushButton*applyButton;
32 32
 };
33 33
 
34
-/*:667*/
34
+/*:696*/
35 35
 #line 39 "./daterangeselector.w"
36 36
 
37 37
 
@@ -62,4 +62,4 @@ int lastIndex;
62 62
 
63 63
 #endif
64 64
 
65
-/*:665*/
65
+/*:694*/

+ 1
- 1
src/daterangeselector.w View File

@@ -81,7 +81,7 @@ class DateRangeSelector : public QWidget
81 81
 @<CustomDateRangePopup implementation@>
82 82
 @<DateRangeSelector implementation@>
83 83
 
84
-#ifdef __unix__
84
+#if __APPLE__
85 85
 #include "moc_daterangeselector.cpp"
86 86
 #endif
87 87
 

+ 2
- 2
src/draglabel.cpp View File

@@ -1,4 +1,4 @@
1
-/*1002:*/
1
+/*1054:*/
2 2
 #line 33 "./scales.w"
3 3
 
4 4
 #include "draglabel.h"
@@ -26,4 +26,4 @@ drag->exec();
26 26
 }
27 27
 }
28 28
 
29
-/*:1002*/
29
+/*:1054*/

+ 2
- 2
src/draglabel.h View File

@@ -1,4 +1,4 @@
1
-/*1001:*/
1
+/*1053:*/
2 2
 #line 13 "./scales.w"
3 3
 
4 4
 #ifndef TypicaDragLabelInclude
@@ -17,4 +17,4 @@ void mousePressEvent(QMouseEvent*event);
17 17
 
18 18
 #endif
19 19
 
20
-/*:1001*/
20
+/*:1053*/

+ 5
- 1
src/graphsettings.w View File

@@ -62,6 +62,10 @@ class GraphSettingsRelativeTab : public QWidget@/
62 62
 @ The constructor sets up the interface and restores any previous values from
63 63
 settings.
64 64
 
65
+The default grid line position has been updated since version 1.8 to match the
66
+number of grid lines present when viewing the graph in Fahrenheit and to
67
+present a slightly wider range where most measurements are expected.
68
+
65 69
 @<GraphSettingsWidget implementation@>=
66 70
 GraphSettingsRelativeTab::GraphSettingsRelativeTab() : QWidget(NULL),
67 71
 	colorEdit(new QLineEdit)
@@ -101,7 +105,7 @@ GraphSettingsRelativeTab::GraphSettingsRelativeTab() : QWidget(NULL),
101 105
 	QHBoxLayout *axisLayout = new QHBoxLayout;
102 106
 	QLabel *axisLabel = new QLabel(tr("Grid line positions (comma separated):"));
103 107
 	QLineEdit *axisEdit = new QLineEdit;
104
-	axisEdit->setText(settings.value("settings/graph/relative/grid", "-300, -100, -10, 0, 10, 30, 50").toString());
108
+	axisEdit->setText(settings.value("settings/graph/relative/grid", "-300, -100, 0, 30, 65, 100").toString());
105 109
 	updateAxisSetting(axisEdit->text());
106 110
 	connect(axisEdit, SIGNAL(textChanged(QString)), this, SLOT(updateAxisSetting(QString)));
107 111
 	axisLayout->addWidget(axisLabel);

+ 7
- 7
src/helpmenu.cpp View File

@@ -1,11 +1,11 @@
1
-/*204:*/
1
+/*207:*/
2 2
 #line 36 "./helpmenu.w"
3 3
 
4 4
 #include "helpmenu.h"
5 5
 #include "abouttypica.h"
6 6
 #include "licensewindow.h"
7 7
 
8
-/*205:*/
8
+/*208:*/
9 9
 #line 46 "./helpmenu.w"
10 10
 
11 11
 HelpMenu::HelpMenu():QMenu()
@@ -24,7 +24,7 @@ connect(licenseAction,SIGNAL(triggered()),this,SLOT(displayLicenseWindow()));
24 24
 #endif
25 25
 }
26 26
 
27
-/*:205*//*206:*/
27
+/*:208*//*209:*/
28 28
 #line 66 "./helpmenu.w"
29 29
 
30 30
 void HelpMenu::displayAboutTypica()
@@ -33,7 +33,7 @@ AboutTypica*aboutBox= new AboutTypica;
33 33
 aboutBox->show();
34 34
 }
35 35
 
36
-/*:206*//*207:*/
36
+/*:209*//*210:*/
37 37
 #line 76 "./helpmenu.w"
38 38
 
39 39
 void HelpMenu::displayLicenseWindow()
@@ -42,11 +42,11 @@ LicenseWindow*window= new LicenseWindow;
42 42
 window->show();
43 43
 }
44 44
 
45
-#line 4772 "./typica.w"
45
+#line 4865 "./typica.w"
46 46
 
47 47
 #line 1 "./licensewindow.w"
48
-/*:207*/
48
+/*:210*/
49 49
 #line 41 "./helpmenu.w"
50 50
 
51 51
 
52
-/*:204*/
52
+/*:207*/

+ 2
- 2
src/helpmenu.h View File

@@ -1,4 +1,4 @@
1
-/*203:*/
1
+/*206:*/
2 2
 #line 16 "./helpmenu.w"
3 3
 
4 4
 #include <QMenu> 
@@ -18,4 +18,4 @@ void displayLicenseWindow();
18 18
 
19 19
 #endif
20 20
 
21
-/*:203*/
21
+/*:206*/

+ 10
- 10
src/licensewindow.cpp View File

@@ -1,7 +1,7 @@
1
-/*209:*/
1
+/*212:*/
2 2
 #line 36 "./licensewindow.w"
3 3
 
4
-/*213:*/
4
+/*216:*/
5 5
 #line 97 "./licensewindow.w"
6 6
 
7 7
 #include "licensewindow.h"
@@ -11,12 +11,12 @@
11 11
 #include <QVariant> 
12 12
 #include <QUrl> 
13 13
 
14
-#line 4774 "./typica.w"
14
+#line 4867 "./typica.w"
15 15
 
16
-/*:213*/
16
+/*:216*/
17 17
 #line 37 "./licensewindow.w"
18 18
 
19
-/*210:*/
19
+/*213:*/
20 20
 #line 43 "./licensewindow.w"
21 21
 
22 22
 LicenseWindow::LicenseWindow()
@@ -25,7 +25,7 @@ LicenseWindow::LicenseWindow()
25 25
 QSplitter*split= new QSplitter;
26 26
 QListWidget*projects= new QListWidget;
27 27
 
28
-/*212:*/
28
+/*215:*/
29 29
 #line 79 "./licensewindow.w"
30 30
 
31 31
 QListWidgetItem*item= new QListWidgetItem("Typica",projects);
@@ -43,7 +43,7 @@ item->setData(Qt::UserRole,QVariant(QUrl("qrc:/resources/html/licenses/qextseria
43 43
 item= new QListWidgetItem("Qt",projects);
44 44
 item->setData(Qt::UserRole,QVariant(QUrl("qrc:/resources/html/licenses/qt.html")));
45 45
 
46
-/*:212*/
46
+/*:215*/
47 47
 #line 50 "./licensewindow.w"
48 48
 
49 49
 connect(projects,SIGNAL(currentItemChanged(QListWidgetItem*,QListWidgetItem*)),
@@ -54,7 +54,7 @@ split->addWidget(view);
54 54
 setCentralWidget(split);
55 55
 }
56 56
 
57
-/*:210*//*211:*/
57
+/*:213*//*214:*/
58 58
 #line 64 "./licensewindow.w"
59 59
 
60 60
 void LicenseWindow::setWebView(QListWidgetItem*current,QListWidgetItem*)
@@ -62,8 +62,8 @@ void LicenseWindow::setWebView(QListWidgetItem*current,QListWidgetItem*)
62 62
 view->load(current->data(Qt::UserRole).toUrl());
63 63
 }
64 64
 
65
-/*:211*/
65
+/*:214*/
66 66
 #line 38 "./licensewindow.w"
67 67
 
68 68
 
69
-/*:209*/
69
+/*:212*/

+ 2
- 2
src/licensewindow.h View File

@@ -1,4 +1,4 @@
1
-/*208:*/
1
+/*211:*/
2 2
 #line 13 "./licensewindow.w"
3 3
 
4 4
 #include <QMainWindow> 
@@ -21,4 +21,4 @@ QWebView*view;
21 21
 
22 22
 #endif
23 23
 
24
-/*:208*/
24
+/*:211*/

+ 285
- 0
src/mergeseries.w View File

@@ -0,0 +1,285 @@
1
+@** Difference and Mean.
2
+
3
+\noindent Some roasters find value in comparing the difference between
4
+measurements on different data series. For example, the difference between
5
+intake and exhaust air or the difference between air and seed temperatures.
6
+These can be expressed as an additional relative temperature series. Similarly,
7
+a roaster might have multiple thermocouples where neither is individually as
8
+reliable as taking the mean between the two. The difference and mean series
9
+allow either of these to be calculated automatically.
10
+
11
+Since these are very similar structurally, a common base class is provided
12
+for performing some calculation based on two inputs. Difference and mean
13
+series then only need to override the |calculate()| method.
14
+
15
+@<Class declarations@>=
16
+class MergeSeries : public QObject
17
+{
18
+	Q_OBJECT
19
+	public:
20
+		MergeSeries();
21
+	public slots:
22
+		void in1(Measurement measure);
23
+		void in2(Measurement measure);
24
+	signals:
25
+		void newData(Measurement measure);
26
+	protected:
27
+		virtual void calculate(Measurement m1, Measurement m2) = 0;
28
+	private:
29
+		Measurement last1;
30
+		Measurement last2;
31
+		bool received1;
32
+		bool received2;
33
+};
34
+
35
+class DifferenceSeries : public MergeSeries
36
+{
37
+	Q_OBJECT
38
+	public:
39
+		DifferenceSeries();
40
+	protected:
41
+		void calculate(Measurement m1, Measurement m2);
42
+};
43
+
44
+class MeanSeries : public MergeSeries
45
+{
46
+	Q_OBJECT
47
+	public:
48
+		MeanSeries();
49
+	protected:
50
+		void calculate(Measurement m1, Measurement m2);
51
+};
52
+
53
+@ Classes derived from |MergeSeries| will wait until there is a measurement
54
+from both series and then call |calculate()| with the most recent measurement
55
+from each. This allows the use of measurements from sources with different
56
+sample rates, though there is no guarantee that measurements with the closest
57
+timestamps will be paired. This is done to minimize latency on the series.
58
+
59
+@<MergeSeries implementation@>=
60
+void MergeSeries::in1(Measurement measure)
61
+{
62
+	last1 = measure;
63
+	received1 = true;
64
+	if(received1 && received2)
65
+	{
66
+		calculate(last1, last2);
67
+		received1 = false;
68
+		received2 = false;
69
+	}
70
+}
71
+
72
+void MergeSeries::in2(Measurement measure)
73
+{
74
+	last2 = measure;
75
+	received2 = true;
76
+	if(received1 && received2)
77
+	{
78
+		calculate(last1, last2);
79
+		received1 = false;
80
+		received2 = false;
81
+	}
82
+}
83
+
84
+@ The constructor just needs to set the initial |bool|s to |false|.
85
+
86
+@<MergeSeries implementation@>=
87
+MergeSeries::MergeSeries() : QObject(NULL), received1(false), received2(false)
88
+{
89
+	/* Nothing needs to be done here. */
90
+}
91
+
92
+@ The calculations will emit a new |Measurement| with the calculated
93
+temperature value. If the measurement times are different, the highest value
94
+is used. A difference needs to be treated as a relative measurement whereas a
95
+mean does not.
96
+
97
+@<MergeSeries implementation@>=
98
+void DifferenceSeries::calculate(Measurement m1, Measurement m2)
99
+{
100
+	Measurement outval(m1.temperature() - m2.temperature(),
101
+	                   (m1.time() > m2.time() ? m1.time() : m2.time()));
102
+	outval.insert("relative", true);
103
+	emit newData(outval);
104
+}
105
+
106
+void MeanSeries::calculate(Measurement m1, Measurement m2)
107
+{
108
+	Measurement outval((m1.temperature() + m2.temperature()) / 2,
109
+                       (m1.time() > m2.time() ? m1.time() : m2.time()));
110
+	emit newData(outval);
111
+}
112
+
113
+@ Nothing special needs to happen in the constructors.
114
+
115
+@<MergeSeries implementation@>=
116
+DifferenceSeries::DifferenceSeries() : MergeSeries()
117
+{
118
+	/* Nothing needs to be done here. */
119
+}
120
+
121
+MeanSeries::MeanSeries() : MergeSeries()
122
+{
123
+	/* Nothing needs to be done here. */
124
+}
125
+
126
+@ The base class does not need to be exposed to the host environment, but the
127
+derived classes do.
128
+
129
+@<Function prototypes for scripting@>=
130
+QScriptValue constructDifferenceSeries(QScriptContext *context,
131
+                                       QScriptEngine *engine);
132
+QScriptValue constructMeanSeries(QScriptContext *context,
133
+                                 QScriptEngine *engine);
134
+
135
+@ The constructors are registered in the usual way.
136
+
137
+@<Set up the scripting engine@>=
138
+constructor = engine->newFunction(constructDifferenceSeries);
139
+value = engine->newQMetaObject(&DifferenceSeries::staticMetaObject,
140
+                               constructor);
141
+engine->globalObject().setProperty("DifferenceSeries", value);
142
+constructor = engine->newFunction(constructMeanSeries);
143
+value = engine->newQMetaObject(&MeanSeries::staticMetaObject, constructor);
144
+engine->globalObject().setProperty("MeanSeries", value);
145
+
146
+@ The constructors are trivial.
147
+
148
+@<Functions for scripting@>=
149
+QScriptValue constructDifferenceSeries(QScriptContext *, QScriptEngine *engine)
150
+{
151
+	QScriptValue object = engine->newQObject(new DifferenceSeries);
152
+	setQObjectProperties(object, engine);
153
+	return object;
154
+}
155
+
156
+QScriptValue constructMeanSeries(QScriptContext *, QScriptEngine *engine)
157
+{
158
+	QScriptValue object = engine->newQObject(new MeanSeries);
159
+	setQObjectProperties(object, engine);
160
+	return object;
161
+}
162
+
163
+@ Both of these require configuration, however since these are structurally
164
+identical, rather than create multiple configuration widgets I have opted to
165
+instead have a selector to choose between the two options.
166
+
167
+@<Class declarations@>=
168
+class MergeSeriesConfWidget : public BasicDeviceConfigurationWidget
169
+{
170
+	Q_OBJECT
171
+	public:
172
+		Q_INVOKABLE MergeSeriesConfWidget(DeviceTreeModel *model,
173
+                                          const QModelIndex &index);
174
+	private slots:
175
+		void updateColumn1(const QString &column);
176
+		void updateColumn2(const QString &column);
177
+		void updateOutput(const QString &column);
178
+		void updateType(int type);
179
+};
180
+
181
+@ The constructor sets up the user interface.
182
+
183
+@<MergeSeriesConfWidget implementation@>=
184
+MergeSeriesConfWidget::MergeSeriesConfWidget(DeviceTreeModel *model,
185
+                                             const QModelIndex &index)
186
+: BasicDeviceConfigurationWidget(model, index)
187
+{
188
+	QFormLayout *layout = new QFormLayout;
189
+	QComboBox *type = new QComboBox;
190
+	type->addItem(tr("Difference"), QVariant("Difference"));
191
+	type->addItem(tr("Mean"), QVariant("Mean"));
192
+	layout->addRow(tr("Series type:"), type);
193
+	QLineEdit *column1 = new QLineEdit;
194
+	layout->addRow(tr("First input column name:"), column1);
195
+	QLineEdit *column2 = new QLineEdit;
196
+	layout->addRow(tr("Second input column name:"), column2);
197
+	QLineEdit *output = new QLineEdit;
198
+	layout->addRow(tr("Output column name:"), output);
199
+	@<Get device configuration data for current node@>@;
200
+	for(int i = 0; i < configData.size(); i++)
201
+	{
202
+		node = configData.at(i).toElement();
203
+		if(node.attribute("name") == "type")
204
+		{
205
+			type->setCurrentIndex(type->findData(node.attribute("value")));
206
+		}
207
+		else if(node.attribute("name") == "column1")
208
+		{
209
+			column1->setText(node.attribute("value"));
210
+		}
211
+		else if(node.attribute("name") == "column2")
212
+		{
213
+			column2->setText(node.attribute("value"));
214
+		}
215
+		else if(node.attribute("name") == "output")
216
+		{
217
+			output->setText(node.attribute("value"));
218
+		}
219
+	}
220
+	updateColumn1(column1->text());
221
+	updateColumn2(column2->text());
222
+	updateOutput(output->text());
223
+	updateType(type->currentIndex());
224
+	connect(column1, SIGNAL(textEdited(QString)), this, SLOT(updateColumn1(QString)));
225
+	connect(column2, SIGNAL(textEdited(QString)), this, SLOT(updateColumn2(QString)));
226
+	connect(output, SIGNAL(textEdited(QString)), this, SLOT(updateOutput(QString)));
227
+	connect(type, SIGNAL(currentIndexChanged(int)), this, SLOT(updateType(int)));
228
+	setLayout(layout);
229
+}
230
+
231
+@ The update methods are trivial.
232
+
233
+@<MergeSeriesConfWidget implementation@>=
234
+void MergeSeriesConfWidget::updateColumn1(const QString &column)
235
+{
236
+	updateAttribute("column1", column);
237
+}
238
+
239
+void MergeSeriesConfWidget::updateColumn2(const QString &column)
240
+{
241
+	updateAttribute("column2", column);
242
+}
243
+
244
+void MergeSeriesConfWidget::updateOutput(const QString &column)
245
+{
246
+	updateAttribute("output", column);
247
+}
248
+
249
+void MergeSeriesConfWidget::updateType(int index)
250
+{
251
+	switch(index)
252
+	{
253
+		case 0:
254
+			updateAttribute("type", "Difference");
255
+			break;
256
+		case 1:
257
+			updateAttribute("type", "Mean");
258
+			break;
259
+		default:
260
+			break;
261
+	}
262
+}
263
+
264
+@ This is registered with the configuration system.
265
+
266
+@<Register device configuration widgets@>=
267
+app.registerDeviceConfigurationWidget("mergeseries",
268
+                                      MergeSeriesConfWidget::staticMetaObject);
269
+
270
+@ This is accessed through the advanced features menu.
271
+
272
+@<Add node inserters to advanced features menu@>=
273
+NodeInserter *mergeSeriesInserter = new NodeInserter(tr("Merge Series"),
274
+                                                     tr("Merge"),
275
+                                                     "mergeseries");
276
+connect(mergeSeriesInserter, SIGNAL(triggered(QString, QString)),
277
+        this, SLOT(insertChildNode(QString, QString)));
278
+advancedMenu->addAction(mergeSeriesInserter);
279
+
280
+@ The class implementations are currently expanded into |"typica.cpp"|.
281
+
282
+@<Class implementations@>=
283
+@<MergeSeriesConfWidget implementation@>
284
+@<MergeSeries implementation@>
285
+

+ 12
- 6
src/moc_scale.cpp View File

@@ -22,7 +22,7 @@ static const uint qt_meta_data_SerialScale[] = {
22 22
        6,       // revision
23 23
        0,       // classname
24 24
        0,    0, // classinfo
25
-       4,   14, // methods
25
+       6,   14, // methods
26 26
        0,    0, // properties
27 27
        0,    0, // enums/sets
28 28
        0,    0, // constructors
@@ -35,7 +35,9 @@ static const uint qt_meta_data_SerialScale[] = {
35 35
  // slots: signature, parameters, type, tag, flags
36 36
       60,   12,   12,   12, 0x0a,
37 37
       67,   12,   12,   12, 0x0a,
38
-      75,   12,   12,   12, 0x08,
38
+      83,   75,   12,   12, 0x0a,
39
+     119,  108,   12,   12, 0x0a,
40
+     149,   12,   12,   12, 0x08,
39 41
 
40 42
        0        // eod
41 43
 };
@@ -43,7 +45,9 @@ static const uint qt_meta_data_SerialScale[] = {
43 45
 static const char qt_meta_stringdata_SerialScale[] = {
44 46
     "SerialScale\0\0weight,unit\0"
45 47
     "newMeasurement(double,Units::Unit)\0"
46
-    "tare()\0weigh()\0dataAvailable()\0"
48
+    "tare()\0weigh()\0command\0setWeighCommand(QString)\0"
49
+    "terminator\0setCommandTerminator(QString)\0"
50
+    "dataAvailable()\0"
47 51
 };
48 52
 
49 53
 void SerialScale::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
@@ -55,7 +59,9 @@ void SerialScale::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id,
55 59
         case 0: _t->newMeasurement((*reinterpret_cast< double(*)>(_a[1])),(*reinterpret_cast< Units::Unit(*)>(_a[2]))); break;
56 60
         case 1: _t->tare(); break;
57 61
         case 2: _t->weigh(); break;
58
-        case 3: _t->dataAvailable(); break;
62
+        case 3: _t->setWeighCommand((*reinterpret_cast< const QString(*)>(_a[1]))); break;
63
+        case 4: _t->setCommandTerminator((*reinterpret_cast< const QString(*)>(_a[1]))); break;
64
+        case 5: _t->dataAvailable(); break;
59 65
         default: ;
60 66
         }
61 67
     }
@@ -93,9 +99,9 @@ int SerialScale::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
93 99
     if (_id < 0)
94 100
         return _id;
95 101
     if (_c == QMetaObject::InvokeMetaMethod) {
96
-        if (_id < 4)
102
+        if (_id < 6)
97 103
             qt_static_metacall(this, _c, _id, _a);
98
-        _id -= 4;
104
+        _id -= 6;
99 105
     }
100 106
     return _id;
101 107
 }

+ 2013
- 819
src/moc_typica.cpp
File diff suppressed because it is too large
View File


+ 748
- 0
src/modbus.w View File

@@ -0,0 +1,748 @@
1
+@** Another Approach for Modbus RTU Support.
2
+
3
+\noindent The original code for dealing with Modbus RTU devices had a number of
4
+limitations. It was awkward to configure, limited to using a single device per
5
+bus, supported a small number of channels, and only worked with fixed point
6
+numeric representations. The following sections are an initial draft of a more
7
+flexible reworking of Modbus RTU support which should be easier to extend to
8
+cover Modbus TCP, allows a wider range of numeric representations, and allows
9
+for the use of any number of devices on the bus and any number of channels.
10
+
11
+Initial support is focused on function 3 and 4 registers with a known
12
+configuration. Outputs and the ability to configure inputs based on the values
13
+at other addresses will be added later. Modbus TCP support is also planned.
14
+
15
+Once feature parity with the old Modbus code is reached, the older code will be
16
+removed.
17
+
18
+Rather than use a single configuration widget for the entire bus, this is now
19
+split to use one configuration widget for the bus and another widget for an
20
+input channel.
21
+
22
+@<Class declarations@>=
23
+class ModbusNGConfWidget : public BasicDeviceConfigurationWidget
24
+{
25
+    Q_OBJECT
26
+    public:
27
+        Q_INVOKABLE ModbusNGConfWidget(DeviceTreeModel *model, const QModelIndex &index);
28
+    private slots:
29
+        void updatePort(const QString &value);
30
+        void updateBaudRate(const QString &value);
31
+        void updateParity(int value);
32
+        void updateFlowControl(int value);
33
+        void updateStopBits(int value);
34
+        void addInput();
35
+    private:
36
+        ParitySelector *m_parity;
37
+        FlowSelector *m_flow;
38
+        StopSelector *m_stop;
39
+};
40
+
41
+@ The configuration widget for the bus only handles the details of the serial
42
+port and allows adding child nodes representing input channels.
43
+
44
+@<ModbusNG implementation@>=
45
+ModbusNGConfWidget::ModbusNGConfWidget(DeviceTreeModel *model, const QModelIndex &index) :
46
+    BasicDeviceConfigurationWidget(model, index), m_parity(new ParitySelector),
47
+    m_flow(new FlowSelector), m_stop(new StopSelector)
48
+{
49
+    QFormLayout *layout = new QFormLayout;
50
+    PortSelector *port = new PortSelector;
51
+    BaudSelector *baud = new BaudSelector;
52
+    QPushButton *newInput = new QPushButton(tr("Add Input Channel"));
53
+    layout->addRow(QString(tr("Port:")), port);
54
+    layout->addRow(QString(tr("Baud rate:")), baud);
55
+    layout->addRow(QString(tr("Parity:")), m_parity);
56
+    layout->addRow(QString(tr("Flow control:")), m_flow);
57
+    layout->addRow(QString(tr("Stop bits:")), m_stop);
58
+    layout->addRow(newInput);
59
+
60
+    @<Get device configuration data for current node@>@;
61
+
62
+    for(int i = 0; i < configData.size(); i++)
63
+    {
64
+        node = configData.at(i).toElement();
65
+        if(node.attribute("name") == "port")
66
+        {
67
+            int idx = port->findText(node.attribute("value"));
68
+            if(idx >= 0)
69
+            {
70
+                port->setCurrentIndex(idx);
71
+            }
72
+            else
73
+            {
74
+                port->addItem(node.attribute("value"));
75
+            }
76
+        }
77
+        else if(node.attribute("name") == "baud")
78
+        {
79
+            baud->setCurrentIndex(baud->findText(node.attribute("value")));
80
+        }
81
+        else if(node.attribute("name") == "parity")
82
+        {
83
+            m_parity->setCurrentIndex(m_parity->findData(node.attribute("value")));
84
+        }
85
+        else if(node.attribute("name") == "flow")
86
+        {
87
+            m_flow->setCurrentIndex(m_flow->findData(node.attribute("value")));
88
+        }
89
+        else if(node.attribute("name") == "stop")
90
+        {
91
+            m_stop->setCurrentIndex(m_stop->findData(node.attribute("value")));
92
+        }
93
+    }
94
+
95
+    updatePort(port->currentText());
96
+    updateBaudRate(baud->currentText());
97
+    updateParity(m_parity->currentIndex());
98
+    updateFlowControl(m_flow->currentIndex());
99
+    updateStopBits(m_stop->currentIndex());
100
+    connect(port, SIGNAL(currentIndexChanged(QString)), this, SLOT(updatePort(QString)));
101
+    connect(port, SIGNAL(editTextChanged(QString)), this, SLOT(updatePort(QString)));
102
+    connect(baud, SIGNAL(currentIndexChanged(QString)), this, SLOT(updateBaudRate(QString)));
103
+    connect(m_parity, SIGNAL(currentIndexChanged(int)), this, SLOT(updateParity(int)));
104
+    connect(m_flow, SIGNAL(currentIndexChanged(int)),
105
+            this, SLOT(updateFlowControl(int)));
106
+    connect(m_stop, SIGNAL(currentIndexChanged(int)), this, SLOT(updateStopBits(int)));
107
+    connect(newInput, SIGNAL(clicked()), this, SLOT(addInput()));
108
+
109
+    setLayout(layout);
110
+}
111
+
112
+void ModbusNGConfWidget::updatePort(const QString &value)
113
+{
114
+    updateAttribute("port", value);
115
+}
116
+
117
+void ModbusNGConfWidget::updateBaudRate(const QString &value)
118
+{
119
+    updateAttribute("baud", value);
120
+}
121
+
122
+void ModbusNGConfWidget::updateParity(int value)
123
+{
124
+    updateAttribute("parity", m_parity->itemData(value).toString());
125
+}
126
+
127
+void ModbusNGConfWidget::updateFlowControl(int value)
128
+{
129
+    updateAttribute("flow", m_flow->itemData(value).toString());
130
+}
131
+
132
+void ModbusNGConfWidget::updateStopBits(int value)
133
+{
134
+    updateAttribute("stop", m_stop->itemData(value).toString());
135
+}
136
+
137
+void ModbusNGConfWidget::addInput()
138
+{
139
+    insertChildNode(tr("Input"), "modbusnginput");
140
+}
141
+
142
+@ Next, there is a configuration widget for input channels.
143
+
144
+@<Class declarations@>=
145
+class ModbusNGInputConfWidget : public BasicDeviceConfigurationWidget
146
+{
147
+    Q_OBJECT
148
+    public:
149
+        Q_INVOKABLE ModbusNGInputConfWidget(DeviceTreeModel *model, const QModelIndex &index);
150
+    private slots:
151
+        void updateStation(int value);
152
+        void updateAddress(int value);
153
+        void updateFunction(int value);
154
+        void updateFormat(int value);
155
+        void updateDecimals(int value);
156
+        void updateUnit(int value);
157
+        void updateColumnName(const QString &value);
158
+        void updateHidden(bool value);
159
+};
160
+
161
+@ This is where the function, address, and additional required details about
162
+an input channel are defind.
163
+
164
+@<ModbusNG implementation@>=
165
+ModbusNGInputConfWidget::ModbusNGInputConfWidget(DeviceTreeModel *model, const QModelIndex &index) : BasicDeviceConfigurationWidget(model, index)
166
+{
167
+    QFormLayout *layout = new QFormLayout;
168
+    QSpinBox *station = new QSpinBox;
169
+    station->setMinimum(1);
170
+    station->setMaximum(247);
171
+    layout->addRow(tr("Station ID"), station);
172
+    QComboBox *function = new QComboBox;
173
+    function->addItem("3", "3");
174
+    function->addItem("4", "4");
175
+    function->setCurrentIndex(1);
176
+    layout->addRow(tr("Function"), function);
177
+    ShortHexSpinBox *address = new ShortHexSpinBox;
178
+    layout->addRow(tr("Address"), address);
179
+    QComboBox *format = new QComboBox;
180
+    format->addItem(tr("16 bits fixed point"), "16fixedint");
181
+    format->addItem(tr("32 bits floating point (High Low)"), "32floathl");
182
+    format->addItem(tr("32 bits floating point (Low High)"), "32floatlh");
183
+    layout->addRow(tr("Data format"), format);
184
+    QSpinBox *decimals = new QSpinBox;
185
+    decimals->setMinimum(0);
186
+    decimals->setMaximum(9);
187
+    layout->addRow(tr("Decimal places"), decimals);
188
+    QComboBox *unit = new QComboBox;
189
+    unit->addItem("Celsius", "C");
190
+    unit->addItem("Fahrenheit", "F");
191
+    unit->addItem("Control", "Control");
192
+    unit->setCurrentIndex(1);
193
+    layout->addRow(tr("Unit"), unit);
194
+    QLineEdit *column = new QLineEdit;
195
+    layout->addRow(tr("Column name"), column);
196
+    QCheckBox *hidden = new QCheckBox(tr("Hide this channel"));
197
+    layout->addRow(hidden);
198
+    
199
+    @<Get device configuration data for current node@>@;
200
+    for(int i = 0; i < configData.size(); i++)
201
+    {
202
+        node = configData.at(i).toElement();
203
+        if(node.attribute("name") == "station")
204
+        {
205
+            station->setValue(node.attribute("value").toInt());
206
+        }
207
+        else if(node.attribute("name") == "function")
208
+        {
209
+            function->setCurrentIndex(function->findText(node.attribute("value")));
210
+        }
211
+        else if(node.attribute("name") == "address")
212
+        {
213
+            address->setValue(node.attribute("value").toInt());
214
+        }
215
+        else if(node.attribute("name") == "format")
216
+        {
217
+            format->setCurrentIndex(format->findData(node.attribute("value")));
218
+        }
219
+        else if(node.attribute("name") == "decimals")
220
+        {
221
+            decimals->setValue(node.attribute("value").toInt());
222
+        }
223
+        else if(node.attribute("name") == "unit")
224
+        {
225
+            unit->setCurrentIndex(unit->findData(node.attribute("value")));
226
+        }
227
+        else if(node.attribute("name") == "column")
228
+        {
229
+            column->setText(node.attribute("value"));
230
+        }
231
+        else if(node.attribute("name") == "hidden")
232
+        {
233
+            hidden->setChecked(node.attribute("value") == "true" ? true : false);
234
+        }
235
+    }
236
+    updateStation(station->value());
237
+    updateFunction(function->currentIndex());
238
+    updateAddress(address->value());
239
+    updateFormat(format->currentIndex());
240
+    updateDecimals(decimals->value());
241
+    updateUnit(unit->currentIndex());
242
+    updateColumnName(column->text());
243
+    updateHidden(hidden->isChecked());
244
+    
245
+    connect(station, SIGNAL(valueChanged(int)), this, SLOT(updateStation(int)));
246
+    connect(function, SIGNAL(currentIndexChanged(int)), this, SLOT(updateFunction(int)));
247
+    connect(address, SIGNAL(valueChanged(int)), this, SLOT(updateAddress(int)));
248
+    connect(format, SIGNAL(currentIndexChanged(int)), this, SLOT(updateFormat(int)));
249
+    connect(decimals, SIGNAL(valueChanged(int)), this, SLOT(updateDecimals(int)));
250
+    connect(unit, SIGNAL(currentIndexChanged(int)), this, SLOT(updateUnit(int)));
251
+    connect(column, SIGNAL(textEdited(QString)), this, SLOT(updateColumnName(QString)));
252
+    connect(hidden, SIGNAL(toggled(bool)), this, SLOT(updateHidden(bool)));
253
+    
254
+    setLayout(layout);
255
+}
256
+
257
+void ModbusNGInputConfWidget::updateStation(int value)
258
+{
259
+    updateAttribute("station", QString("%1").arg(value));
260
+}
261
+
262
+void ModbusNGInputConfWidget::updateFunction(int  value)
263
+{
264
+    updateAttribute("function", QString("%1").arg(value == 0 ? "3" : "4"));
265
+}
266
+
267
+void ModbusNGInputConfWidget::updateAddress(int value)
268
+{
269
+    updateAttribute("address", QString("%1").arg(value));
270
+}
271
+
272
+void ModbusNGInputConfWidget::updateFormat(int value)
273
+{
274
+    switch(value)
275
+    {
276
+        case 0:
277
+            updateAttribute("format", "16fixedint");
278
+            break;
279
+        case 1:
280
+            updateAttribute("format", "32floathl");
281
+            break;
282
+        case 2:
283
+            updateAttribute("format", "32floatlh");
284
+            break;
285
+    }
286
+}
287
+
288
+void ModbusNGInputConfWidget::updateDecimals(int value)
289
+{
290
+    updateAttribute("decimals", QString("%1").arg(value));
291
+}
292
+
293
+void ModbusNGInputConfWidget::updateUnit(int value)
294
+{
295
+    switch(value)
296
+    {
297
+        case 0:
298
+            updateAttribute("unit", "C");
299
+            break;
300
+        case 1:
301
+            updateAttribute("unit", "F");
302
+            break;
303
+        case 2:
304
+	        updateAttribute("unit", "Control");
305
+	        break;
306
+    }
307
+}
308
+
309
+void ModbusNGInputConfWidget::updateColumnName(const QString &value)
310
+{
311
+    updateAttribute("column", value);
312
+}
313
+
314
+void ModbusNGInputConfWidget::updateHidden(bool value)
315
+{
316
+    updateAttribute("hidden", value ? "true" : "false");
317
+}
318
+
319
+@ The configuration widgets need to be registered.
320
+
321
+@<Register device configuration widgets@>=
322
+app.registerDeviceConfigurationWidget("modbusngport", ModbusNGConfWidget::staticMetaObject);
323
+app.registerDeviceConfigurationWidget("modbusnginput",
324
+                                      ModbusNGInputConfWidget::staticMetaObject);
325
+
326
+@ A |NodeInserter| is also needed.
327
+
328
+@<Register top level device configuration nodes@>=
329
+inserter = new NodeInserter(tr("ModbusNG Port"), tr("Modbus RTU Port"), "modbusngport", NULL);
330
+topLevelNodeInserters.append(inserter);
331
+
332
+@ While the old design only needed to deal with a small number of potential
333
+messages and responses, it makes sense for the new design to use a scan list
334
+of arbitrary length. An initial implementation can simply store the data needed
335
+to make requests, properly interpret the results, and output data to the
336
+correct channels. There is room to improve operational efficiency later by
337
+batching operations on adjacent addresses on the same function into a single
338
+request, but there are known devices in use in coffee roasters which do not
339
+support reading from multiple registers simultaneously, so there must also be a
340
+way to turn such optimizations off.
341
+
342
+@<Class declarations@>=
343
+enum ModbusDataFormat
344
+{
345
+    Int16,
346
+    FloatHL,
347
+    FloatLH
348
+};
349
+
350
+struct ModbusScanItem
351
+{
352
+    QByteArray request;
353
+    ModbusDataFormat format;
354
+    int decimalPosition;
355
+    Units::Unit unit;
356
+    mutable double lastValue;
357
+};
358
+
359
+@ Another class is used to handle the communication with the bus and serve as
360
+an integration point for \pn{}.
361
+
362
+@<Class declarations@>=
363
+class ModbusNG : public QObject
364
+{
365
+    Q_OBJECT
366
+    public:
367
+        ModbusNG(DeviceTreeModel *model, const QModelIndex &index);
368
+        ~ModbusNG();
369
+        Q_INVOKABLE int channelCount();
370
+        Channel* getChannel(int);
371
+        Q_INVOKABLE QString channelColumnName(int);
372
+        Q_INVOKABLE QString channelIndicatorText(int);
373
+        Q_INVOKABLE bool isChannelHidden(int);
374
+        Q_INVOKABLE QString channelType(int);
375
+    private slots:
376
+        void sendNextMessage();
377
+        void timeout();
378
+        void dataAvailable();
379
+        void rateLimitTimeout();
380
+    private:
381
+        quint16 calculateCRC(QByteArray data);
382
+        QextSerialPort *port;
383
+        int delayTime;
384
+        QTimer *messageDelayTimer;
385
+        QTimer *commTimeout;
386
+        QTimer *rateLimiter;
387
+        int scanPosition;
388
+        bool waiting;
389
+        QByteArray responseBuffer;
390
+        QList<Channel*> channels;
391
+        QList<ModbusScanItem> scanList;
392
+        QList<QString> channelNames;
393
+        QList<QString> channelLabels;
394
+        QList<bool> hiddenStates;
395
+        QList<QString> channelTypeList;
396
+        QVector<double> lastMeasurement;
397
+};
398
+
399
+@ One of the things that the old Modbus code got right was in allowing the
400
+constructor to handle device configuration by accepting its configuration
401
+sub-tree. In this design, child nodes establish a scan list.
402
+
403
+@<ModbusNG implementation@>=
404
+ModbusNG::ModbusNG(DeviceTreeModel *model, const QModelIndex &index) :
405
+    QObject(NULL), messageDelayTimer(new QTimer), commTimeout(new QTimer),
406
+    rateLimiter(new QTimer), scanPosition(0), waiting(false)
407
+{
408
+    QDomElement portReferenceElement =
409
+        model->referenceElement(model->data(index, Qt::UserRole).toString());
410
+    QDomNodeList portConfigData = portReferenceElement.elementsByTagName("attribute");
411
+    QDomElement node;
412
+    QVariantMap attributes;
413
+    for(int i = 0; i < portConfigData.size(); i++)
414
+    {
415
+        node = portConfigData.at(i).toElement();
416
+        attributes.insert(node.attribute("name"), node.attribute("value"));
417
+    }
418
+    port = new QextSerialPort(attributes.value("port").toString(),
419
+                              QextSerialPort::EventDriven);
420
+    port->setBaudRate((BaudRateType)(attributes.value("baud").toInt()));
421
+    port->setDataBits(DATA_8);
422
+    port->setParity((ParityType)attributes.value("parity").toInt());
423
+    port->setStopBits((StopBitsType)attributes.value("stop").toInt());
424
+    port->setFlowControl((FlowType)attributes.value("flow").toInt());
425
+    delayTime = (int)(((double)(1)/(double)(attributes.value("baud").toInt())) * 144000.0);
426
+    messageDelayTimer->setSingleShot(true);
427
+    commTimeout->setSingleShot(true);
428
+    rateLimiter->setSingleShot(true);
429
+    rateLimiter->setInterval(0);
430
+    connect(messageDelayTimer, SIGNAL(timeout()), this, SLOT(sendNextMessage()));
431
+    connect(commTimeout, SIGNAL(timeout()), this, SLOT(timeout()));
432
+    connect(port, SIGNAL(readyRead()), this, SLOT(dataAvailable()));
433
+    connect(rateLimiter, SIGNAL(timeout()), this, SLOT(rateLimitTimeout()));
434
+    if(!port->open(QIODevice::ReadWrite))
435
+    {
436
+	    qDebug() << "Failed to open serial port";
437
+    }
438
+    for(int i = 0; i < model->rowCount(index); i++)
439
+    {
440
+        QModelIndex channelIndex = model->index(i, 0, index);
441
+        QDomElement channelReferenceElement =
442
+            model->referenceElement(model->data(channelIndex, Qt::UserRole).toString());
443
+        QDomNodeList channelConfigData =
444
+            channelReferenceElement.elementsByTagName("attribute");
445
+        QDomElement channelNode;
446
+        QVariantMap channelAttributes;
447
+        for(int j = 0; j < channelConfigData.size(); j++)
448
+        {
449
+            channelNode = channelConfigData.at(j).toElement();
450
+            channelAttributes.insert(channelNode.attribute("name"),
451
+                                     channelNode.attribute("value"));
452
+        }
453
+        ModbusScanItem scanItem;
454
+        QString format = channelAttributes.value("format").toString();
455
+        if(format == "16fixedint")
456
+        {
457
+            scanItem.format = Int16;
458
+        }
459
+        else if(format == "32floathl")
460
+        {
461
+            scanItem.format = FloatHL;
462
+        }
463
+        else if(format == "32floatlh")
464
+        {
465
+            scanItem.format = FloatLH;
466
+        }
467
+        scanItem.request.append((char)channelAttributes.value("station").toInt());
468
+        scanItem.request.append((char)channelAttributes.value("function").toInt());
469
+        quint16 startAddress = (quint16)channelAttributes.value("address").toInt();
470
+        char *startAddressBytes = (char*)&startAddress;
471
+        scanItem.request.append(startAddressBytes[1]);
472
+        scanItem.request.append(startAddressBytes[0]);
473
+        scanItem.request.append((char)0x00);
474
+        if(scanItem.format == Int16)
475
+        {
476
+            scanItem.request.append((char)0x01);
477
+        }
478
+        else
479
+        {
480
+            scanItem.request.append((char)0x02);
481
+        }
482
+        quint16 crc = calculateCRC(scanItem.request);
483
+        char *crcBytes = (char*)&crc;
484
+        scanItem.request.append(crcBytes[0]);
485
+        scanItem.request.append(crcBytes[1]);
486
+        scanItem.decimalPosition = channelAttributes.value("decimals").toInt();
487
+        if(channelAttributes.value("unit").toString() == "C")
488
+        {
489
+            scanItem.unit = Units::Celsius;
490
+            channelTypeList.append("T");
491
+        }
492
+        else if(channelAttributes.value("unit").toString() == "F")
493
+        {
494
+            scanItem.unit = Units::Fahrenheit;
495
+            channelTypeList.append("T");
496
+        }
497
+        else
498
+        {
499
+	        scanItem.unit = Units::Unitless;
500
+	        channelTypeList.append("C");
501
+        }
502
+        scanList.append(scanItem);
503
+        lastMeasurement.append(0.0);
504
+        channels.append(new Channel);
505
+        channelNames.append(channelAttributes.value("column").toString());
506
+        hiddenStates.append(
507
+            channelAttributes.value("hidden").toString() == "true" ? true : false);
508
+        channelLabels.append(model->data(channelIndex, 0).toString());
509
+    }
510
+    messageDelayTimer->start();
511
+}
512
+
513
+ModbusNG::~ModbusNG()
514
+{
515
+    commTimeout->stop();
516
+    messageDelayTimer->stop();
517
+    port->close();
518
+}
519
+
520
+void ModbusNG::sendNextMessage()
521
+{
522
+    if(scanList.length() > 0 && !waiting)
523
+    {
524
+        port->write(scanList.at(scanPosition).request);
525
+        commTimeout->start(2000);
526
+        messageDelayTimer->start(delayTime);
527
+        waiting = true;
528
+    }
529
+}
530
+
531
+void ModbusNG::timeout()
532
+{
533
+    qDebug() << "Communications timeout.";
534
+    messageDelayTimer->start();
535
+}
536
+
537
+void ModbusNG::rateLimitTimeout()
538
+{
539
+	messageDelayTimer->start();
540
+}
541
+
542
+void ModbusNG::dataAvailable()
543
+{
544
+    if(messageDelayTimer->isActive())
545
+    {
546
+        messageDelayTimer->stop();
547
+    }
548
+    responseBuffer.append(port->readAll());
549
+    if(responseBuffer.size() < 5)
550
+    {
551
+        return;
552
+    }
553
+    if(responseBuffer.size() < 5 + responseBuffer.at(2))
554
+    {
555
+        return;
556
+    }
557
+    responseBuffer = responseBuffer.left(5 + responseBuffer.at(2));
558
+    commTimeout->stop();
559
+    if(calculateCRC(responseBuffer) == 0)
560
+    {
561
+        quint16 intresponse;
562
+        float floatresponse;
563
+        char *ibytes = (char*)&intresponse;
564
+        char *fbytes = (char*)&floatresponse;
565
+        double output;
566
+        switch(scanList.at(scanPosition).format)
567
+        {
568
+            case Int16:
569
+                ibytes[0] = responseBuffer.at(4);
570
+                ibytes[1] = responseBuffer.at(3);
571
+                output = intresponse;
572
+                for(int i = 0; i < scanList.at(scanPosition).decimalPosition; i++)
573
+                {
574
+                    output /= 10;
575
+                }
576
+                break;
577
+            case FloatHL:
578
+                fbytes[0] = responseBuffer.at(4);
579
+                fbytes[1] = responseBuffer.at(3);
580
+                fbytes[2] = responseBuffer.at(6);
581
+                fbytes[3] = responseBuffer.at(5);
582
+                output = floatresponse;
583
+                break;
584
+            case FloatLH:
585
+                fbytes[0] = responseBuffer.at(6);
586
+                fbytes[1] = responseBuffer.at(5);
587
+                fbytes[2] = responseBuffer.at(4);
588
+                fbytes[3] = responseBuffer.at(3);
589
+                output = floatresponse;
590
+                break;
591
+        }
592
+        if(scanList.at(scanPosition).unit == Units::Celsius)
593
+        {
594
+            output = output * 9.0 / 5.0 + 32.0;
595
+        }
596
+        scanList.at(scanPosition).lastValue = output;
597
+    }
598
+    else
599
+    {
600
+        qDebug() << "CRC failed";
601
+    }
602
+    scanPosition = (scanPosition + 1) % scanList.size();
603
+    if(scanPosition == 0)
604
+    {
605
+        QTime time = QTime::currentTime();
606
+        bool doOutput = false;
607
+        for(int i = 0; i < scanList.size(); i++)
608
+        {
609
+	        if(scanList.at(i).lastValue != lastMeasurement.at(i))
610
+	        {
611
+		        doOutput = true;
612
+		        break;
613
+	        }
614
+        }
615
+        if(doOutput)
616
+        {
617
+	        for(int i = 0; i < scanList.size(); i++)
618
+	        {
619
+		        lastMeasurement[i] = scanList.at(i).lastValue;
620
+		        if(scanList.at(scanPosition).unit == Units::Unitless)
621
+		        {
622
+			        channels.at(i)->input(Measurement(scanList.at(i).lastValue, time, Units::Unitless));
623
+		        }
624
+		        else
625
+		        {
626
+		            channels.at(i)->input(Measurement(scanList.at(i).lastValue, time, Units::Fahrenheit));
627
+	            }
628
+            }
629
+        }
630
+    }
631
+    responseBuffer.clear();
632
+    waiting = false;
633
+    if(scanPosition == 0)
634
+    {
635
+	    rateLimiter->start();
636
+    }
637
+    else
638
+    {
639
+	    messageDelayTimer->start(delayTime);
640
+    }
641
+}
642
+
643
+quint16 ModbusNG::calculateCRC(QByteArray data)
644
+{
645
+    quint16 retval = 0xFFFF;
646
+    int i = 0;
647
+    while(i < data.size())
648
+    {
649
+        retval ^= 0x00FF & (quint16)data.at(i);
650
+        for(int j = 0; j < 8; j++)
651
+        {
652
+            if(retval & 1)
653
+            {
654
+                retval = (retval >> 1) ^ 0xA001;
655
+            }
656
+            else
657
+            {
658
+                retval >>= 1;
659
+            }
660
+        }
661
+        i++;
662
+    }
663
+    return retval;
664
+}
665
+
666
+int ModbusNG::channelCount()
667
+{
668
+    return channels.size();
669
+}
670
+
671
+Channel* ModbusNG::getChannel(int channel)
672
+{
673
+    return channels.at(channel);
674
+}
675
+
676
+QString ModbusNG::channelColumnName(int channel)
677
+{
678
+    return channelNames.at(channel);
679
+}
680
+
681
+QString ModbusNG::channelIndicatorText(int channel)
682
+{
683
+    return channelLabels.at(channel);
684
+}
685
+
686
+bool ModbusNG::isChannelHidden(int channel)
687
+{
688
+    return hiddenStates.at(channel);
689
+}
690
+
691
+QString ModbusNG::channelType(int channel)
692
+{
693
+	return channelTypeList.at(channel);
694
+}
695
+
696
+@ This class must be exposed to the host environment.
697
+
698
+@<Function prototypes for scripting@>=
699
+QScriptValue constructModbusNG(QScriptContext *context, QScriptEngine *engine);
700
+void setModbusNGProperties(QScriptValue value, QScriptEngine *engine);
701
+QScriptValue ModbusNG_getChannel(QScriptContext *context, QScriptEngine *engine);
702
+
703
+@ The host environment is informed of the constructor.
704
+
705
+@<Set up the scripting engine@>=
706
+constructor = engine->newFunction(constructModbusNG);
707
+value = engine->newQMetaObject(&ModbusNG::staticMetaObject, constructor);
708
+engine->globalObject().setProperty("ModbusNG", value);
709
+
710
+@ The constructor takes the configuration model and the index to the device as
711
+arguments.
712
+
713
+@<Functions for scripting@>=
714
+QScriptValue constructModbusNG(QScriptContext *context, QScriptEngine *engine)
715
+{
716
+    QScriptValue object;
717
+    if(context->argumentCount() == 2)
718
+    {
719
+        object = engine->newQObject(new ModbusNG(argument<DeviceTreeModel *>(0, context),
720
+                                                 argument<QModelIndex>(1, context)),
721
+                                    QScriptEngine::ScriptOwnership);
722
+        setModbusNGProperties(object, engine);
723
+    }
724
+    else
725
+    {
726
+        context->throwError("Incorrect number of arguments passed to "@|
727
+            "ModbusNG constructor.");
728
+    }
729
+    return object;
730
+}
731
+
732
+void setModbusNGProperties(QScriptValue value, QScriptEngine *engine)
733
+{
734
+    setQObjectProperties(value, engine);
735
+    value.setProperty("getChannel", engine->newFunction(ModbusNG_getChannel));
736
+}
737
+
738
+QScriptValue ModbusNG_getChannel(QScriptContext *context, QScriptEngine *engine)
739
+{
740
+    ModbusNG *self = getself<ModbusNG *>(context);
741
+    QScriptValue object;
742
+    if(self)
743
+    {
744
+        object = engine->newQObject(self->getChannel(argument<int>(0, context)));
745
+        setChannelProperties(object, engine);
746
+    }
747
+    return object;
748
+}

+ 162
- 0
src/plugins.w View File

@@ -0,0 +1,162 @@
1
+@** Simple Plugins.
2
+
3
+\noindent The original motivation for this feature is to provide a simple way
4
+to allow importing data from other data logging applications. The problem is
5
+that there are huge differences in the data formats exported by different
6
+applications, sometimes there are differences that depend on how the other
7
+application was configured which cannot be reliably determined in an automated
8
+fashion, and if a substantial number of import plugins were created, any given
9
+person using Typica would be unlikely to ever use most of them.
10
+
11
+Based on these concerns, I wanted something that would make it easy to create
12
+new import plugins without the need to create a new build of Typica every time,
13
+I wanted it to be relatively easy for people to modify example import plugins
14
+to suit the data they wanted to import, and I wanted it to be easy for people
15
+to hide plugins that they were not required.
16
+
17
+This is handled in a way similar to reports. A new directory is provided with
18
+the \pn{} configuration which contains files with script code. A menu item is
19
+available that will examine the files in that folder to populate its sub-menu.
20
+
21
+@<Process plugin item@>=
22
+QMenu *pluginMenu = new QMenu(menu);
23
+if(itemElement.hasAttribute("id"))
24
+{
25
+	pluginMenu->setObjectName(itemElement.attribute("id"));
26
+}
27
+if(itemElement.hasAttribute("title"))
28
+{
29
+	pluginMenu->setTitle(itemElement.attribute("title"));
30
+}
31
+if(itemElement.hasAttribute("src"))
32
+{
33
+	QSettings settings;
34
+	QString pluginDirectory = QString("%1/%2").
35
+		arg(settings.value("config").toString()).
36
+		arg(itemElement.attribute("src"));
37
+	QDir directory(pluginDirectory);
38
+	directory.setFilter(QDir::Files);
39
+	directory.setSorting(QDir::Name);
40
+	QStringList nameFilter;
41
+	nameFilter << "*.js";
42
+	directory.setNameFilters(nameFilter);
43
+	QFileInfoList pluginFiles = directory.entryInfoList();
44
+	for(int k = 0; k < pluginFiles.size(); k++)
45
+	{
46
+		PluginAction *pa = new PluginAction(pluginFiles.at(k), pluginMenu);
47
+		if(itemElement.hasAttribute("preRun"))
48
+		{
49
+			pa->setPreRun(itemElement.attribute("preRun"));
50
+		}
51
+		if(itemElement.hasAttribute("postRun"))
52
+		{
53
+			pa->setPostRun(itemElement.attribute("postRun"));
54
+		}
55
+		pluginMenu->addAction(pa);
56
+	}
57
+}
58
+menu->addMenu(pluginMenu);
59
+
60
+@ The sub-menu items are a subclass of |QAction| which holds all of the
61
+information needed to respond to its activation.
62
+
63
+@<Class declarations@>=
64
+class PluginAction : public QAction
65
+{
66
+	Q_OBJECT
67
+	Q_PROPERTY(QString preRun READ preRun WRITE setPreRun);
68
+	Q_PROPERTY(QString postRun READ postRun WRITE setPostRun);
69
+	public:
70
+		PluginAction(const QFileInfo &info, QObject *parent);
71
+		QString preRun();
72
+		QString postRun();
73
+	public slots:
74
+		void setPreRun(const QString &script);
75
+		void setPostRun(const QString &script);
76
+	private slots:
77
+		void runScript();
78
+	private:
79
+		QString pluginFile;
80
+		QString preRunScript;
81
+		QString postRunScript;
82
+};
83
+
84
+@ The constructor takes a |QFileInfo| and uses that to extract the path of the
85
+file used to respond to the action activation as well as the text that should
86
+be used in the menu text. It also takes a |QObject*| parent which should be the
87
+|QMenu| the |PluginAction| will be placed in.
88
+
89
+Everything interesting happens in |runScript()| which is called when the action
90
+is triggered.
91
+
92
+@<PluginAction implementation@>=
93
+PluginAction::PluginAction(const QFileInfo &info, QObject *parent) :
94
+	QAction(parent), preRunScript(""), postRunScript("")
95
+{
96
+	pluginFile = info.absoluteFilePath();
97
+	setText(info.baseName());
98
+	connect(this, SIGNAL(triggered()), this, SLOT(runScript()));
99
+}
100
+
101
+void PluginAction::runScript()
102
+{
103
+	QFile file(pluginFile);
104
+	if(file.open(QIODevice::ReadOnly))
105
+	{
106
+		QScriptEngine *engine = AppInstance->engine;
107
+		QScriptContext *context = engine->pushContext();
108
+		if(parent()->dynamicPropertyNames().contains("activationObject"))
109
+		{
110
+			QScriptValue activationObject =
111
+				parent()->property("activationObject").value<QScriptValue>();
112
+			context->setActivationObject(activationObject);
113
+		}
114
+		QString script(file.readAll());
115
+		QScriptValue retval = engine->evaluate(preRunScript + script + postRunScript, pluginFile);
116
+		if(engine->hasUncaughtException())
117
+		{
118
+			qDebug() << "Uncaught exception: " <<
119
+				engine->uncaughtException().toString() <<
120
+				" in " << pluginFile << " line: " <<
121
+				engine->uncaughtExceptionLineNumber();
122
+		}
123
+		engine->popContext();
124
+		file.close();
125
+	}
126
+}
127
+
128
+@ Pre-run and post-run scripts can be set to handle boilerplate that would
129
+otherwise need to be included in all plugins.
130
+
131
+@<PluginAction implementation@>=
132
+QString PluginAction::preRun()
133
+{
134
+	return preRunScript;
135
+}
136
+
137
+QString PluginAction::postRun()
138
+{
139
+	return postRunScript;
140
+}
141
+
142
+void PluginAction::setPreRun(const QString &script)
143
+{
144
+	preRunScript = script;
145
+}
146
+
147
+void PluginAction::setPostRun(const QString &script)
148
+{
149
+	postRunScript = script;
150
+}
151
+
152
+@ In order to get the activation object in this way, we need to allow a
153
+|QScriptValue| to be stored in a |QVariant|.
154
+
155
+@<Class declarations@>=
156
+Q_DECLARE_METATYPE(QScriptValue)
157
+
158
+@ This is added to the list of class implementations.
159
+
160
+@<Class implementations@>=
161
+@<PluginAction implementation@>
162
+

+ 22
- 0
src/printerselector.cpp View File

@@ -0,0 +1,22 @@
1
+/*590:*/
2
+#line 45 "./printerselector.w"
3
+
4
+#include "printerselector.h"
5
+
6
+/*591:*/
7
+#line 53 "./printerselector.w"
8
+
9
+PrinterSelector::PrinterSelector():QComboBox(NULL)
10
+{
11
+QList<QPrinterInfo> printers= QPrinterInfo::availablePrinters();
12
+foreach(QPrinterInfo info,printers)
13
+{
14
+addItem(info.printerName());
15
+}
16
+}
17
+
18
+/*:591*/
19
+#line 48 "./printerselector.w"
20
+
21
+
22
+/*:590*/

+ 19
- 0
src/printerselector.h View File

@@ -0,0 +1,19 @@
1
+/*588:*/
2
+#line 22 "./printerselector.w"
3
+
4
+#include <QPrinterInfo> 
5
+#include <QComboBox> 
6
+
7
+#ifndef TypicaPrinterSelectorHeader
8
+#define TypicaPrinterSelectorHeader
9
+
10
+class PrinterSelector:public QComboBox
11
+{
12
+Q_OBJECT
13
+public:
14
+PrinterSelector();
15
+};
16
+
17
+#endif
18
+
19
+/*:588*/

+ 116
- 0
src/printerselector.w View File

@@ -0,0 +1,116 @@
1
+@* Saved Printers.
2
+
3
+\noindent In most cases it's best to handle printing in a way that is common
4
+across many applications. Put a Print menu option in a File menu, bring up the
5
+platform's standard print dialog, and allow people to take full advantage of
6
+the flexibility this provides.
7
+
8
+In more specialized use cases, however, it may make more sense to provide
9
+faster access to a printer that might not be the default printer for that
10
+computer. The first use in Typica where this makes sense is in printing tags
11
+that can follow the coffee and uniquely identify that batch. Using a full sheet
12
+of paper for this might be excessive and time consuming. Instead, it might make
13
+sense to get a small, inexpensive thermal receipt printer to keep at the
14
+roaster. If this were not the default printer, it would quickly become tedious
15
+to bring up the print dialog and change the selected printer after every batch.
16
+
17
+In cases like this, it would be better to provide a combo box in the window
18
+where a printer can be selected and remembered as the default printer just for
19
+that particular use, and allowing people to print directly to that printer
20
+without going through extra steps.
21
+
22
+@(printerselector.h@>=
23
+#include <QPrinterInfo>
24
+#include <QComboBox>
25
+
26
+#ifndef TypicaPrinterSelectorHeader
27
+#define TypicaPrinterSelectorHeader
28
+
29
+class PrinterSelector : public QComboBox@/
30
+{
31
+	@[Q_OBJECT@]@;
32
+	public:
33
+		PrinterSelector();
34
+};
35
+
36
+#endif
37
+
38
+@ The main file also requires this header.
39
+
40
+@<Header files to include@>=
41
+#include "printerselector.h"
42
+
43
+@ Implementation of this class is in a separate file.
44
+
45
+@(printerselector.cpp@>=
46
+#include "printerselector.h"
47
+
48
+@<PrinterSelector implementation@>@;
49
+
50
+@ The constructor looks at the list of available printers and populates itself
51
+with these.
52
+
53
+@<PrinterSelector implementation@>=
54
+PrinterSelector::PrinterSelector() : QComboBox(NULL)
55
+{
56
+	QList<QPrinterInfo> printers = QPrinterInfo::availablePrinters();
57
+	foreach(QPrinterInfo info, printers)
58
+	{
59
+		addItem(info.printerName());
60
+	}
61
+}
62
+
63
+@ The host environment is informed of this class in the usual way starting with
64
+a constructor function prototype. Another prototype is also needed for adding
65
+this to a layout from XML.
66
+
67
+@<Function prototypes for scripting@>=
68
+QScriptValue constructPrinterSelector(QScriptContext *context,
69
+                                      QScriptEngine *engine);
70
+void addPrinterSelectorToLayout(QDomElement element,
71
+                                QStack<QWidget *> *widgetStack,
72
+                                QStack<QLayout *> *layoutStack);
73
+
74
+@ The engine is informed of this function.
75
+
76
+@<Set up the scripting engine@>=
77
+constructor = engine->newFunction(constructPrinterSelector);
78
+engine->globalObject().setProperty("PrinterSelector", constructor);
79
+
80
+@ There is nothing special about the constructor. If there were additional
81
+properties needed beyond those supplied by |setQComboBoxProperties()| it would
82
+make sense to add another function to the chain for setting script value
83
+properties.
84
+
85
+@<Functions for scripting@>=
86
+QScriptValue constructPrinterSelector(QScriptContext *, QScriptEngine *engine)
87
+{
88
+	QScriptValue object = engine->newQObject(new PrinterSelector);
89
+	setQComboBoxProperties(object, engine);
90
+	return object;
91
+}
92
+
93
+@ It should also be possible to add this to a layout from the XML portion of
94
+the configuration document.
95
+
96
+@<Functions for scripting@>=
97
+void addPrinterSelectorToLayout(QDomElement element, QStack<QWidget *> *,
98
+                                QStack<QLayout *> *layoutStack)
99
+{
100
+	PrinterSelector *selector = new PrinterSelector;
101
+	if(element.hasAttribute("id"))
102
+	{
103
+		selector->setObjectName(element.attribute("id"));
104
+	}
105
+	QBoxLayout *layout = qobject_cast<QBoxLayout *>(layoutStack->top());
106
+	layout->addWidget(selector);
107
+}
108
+
109
+@ This is added in the usual way.
110
+
111
+@<Additional box layout elements@>=
112
+else if(currentElement.tagName() == "printerselector")
113
+{
114
+	addPrinterSelectorToLayout(currentElement, widgetStack, layoutStack);
115
+}
116
+

+ 866
- 792
src/qrc_resources.cpp
File diff suppressed because it is too large
View File


+ 4
- 4
src/resources/Info.plist View File

@@ -7,7 +7,7 @@
7 7
 	<key>CFBundlePackageType</key>
8 8
 	<string>APPL</string>
9 9
 	<key>CFBundleGetInfoString</key>
10
-	<string>Typica 1.7.0</string>
10
+	<string>Typica 1.8.0</string>
11 11
 	<key>CFBundleSignature</key>
12 12
 	<string>@TYPEINFO@</string>
13 13
 	<key>CFBundleExecutable</key>
@@ -17,10 +17,10 @@
17 17
 	<key>CFBundleDisplayName</key>
18 18
 	<string>Typica</string>
19 19
 	<key>CFBundleShortVersionString</key>
20
-	<string>1.7.0</string>
20
+	<string>1.8.0</string>
21 21
 	<key>CFBundleVersion</key>
22
-	<string>1.7.0</string>
22
+	<string>1.8.0</string>
23 23
 	<key>NSHumanReadableCopyright</key>
24
-	<string>© 2007–2016 Neal Wilson</string>
24
+	<string>© 2007–2017 Neal Wilson</string>
25 25
 </dict>
26 26
 </plist>

+ 17
- 13
src/resources/html/about.html View File

@@ -1,32 +1,30 @@
1 1
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg.dtd">
2 2
 <html xmlns="http://www.w3.org/1999/xhtml">
3 3
 	<head>
4
-		<title>Typica - Data for Coffee Roasters</title>
4
+		<title>Typica - Software for Coffee Roasting Operations</title>
5 5
 		<link rel="stylesheet" type="text/css" href="style.css" />
6 6
 	</head>
7 7
 	<body>
8 8
 		<div id="page">
9 9
 			<div id="topmatter">
10 10
 				<div class="topbanner">
11
-					<a href="http://www.randomfield.com/programs/typica/"><img src="../icons/appicons/logo96.png" height="96px" width="96px" alt="Typica logo" /></a>
12
-					<h1><a href="http://www.randomfield.com/programs/typica/">Typica</a></h1>
13
-					<h2>Version 1.7.0</h2>
11
+					<a href="https://typica.us/"><img src="../icons/appicons/logo96.png" height="96px" width="96px" alt="Typica logo" /></a>
12
+					<h1><a href="https://typica.us/">Typica</a></h1>
13
+					<h2>Version 1.8.0</h2>
14 14
 				</div>
15 15
 			<div id="maintext">
16
-				<p>Copyright &copy; 2007&ndash;2016 Neal Evan Wilson
16
+				<p>Copyright &copy; 2007&ndash;2017 Neal Evan Wilson
17 17
 					<span class="icons">
18
-						<a href="mailto:roaster@wilsonscoffee.com?subject=Thanks%20for%20Typica&amp;body=Message%20initiated%20from%20Typica.">&#9993;</a>
18
+						<a href="mailto:neal@typica.us?subject=Thanks%20for%20Typica&amp;body=Message%20initiated%20from%20Typica.">&#9993;</a>
19 19
 						<a href="https://twitter.com/N3Roaster">&#62217;</a>
20 20
 						<a href="https://github.com/N3Roaster">&#62208;</a>
21
-						<a href="http://appliedcoffeetechnology.tumblr.com/">&#62229;</a>
22
-						<a href="http://www.linkedin.com/profile/view?id=179814079">&#62232;</a>
23 21
 					</span>
24 22
 				</p>
25 23
 				<p>German Translation: Mario Champignon
26
-                                    <span class="icons">
27
-                                        <a href="mailto:mario_champignon@hotmail.com">&#9993;</a>
28
-                                    </span>
29
-                                </p>
24
+                    <span class="icons">
25
+                        <a href="mailto:mario_champignon@hotmail.com">&#9993;</a>
26
+                    </span>
27
+                </p>
30 28
 				
31 29
 				<h3>License Information</h3>
32 30
 				<p>Permission is hereby granted, free of charge, to any person
@@ -93,7 +91,13 @@
93 91
 					<li>The name Michael Bostock may not be used to endorse or promote products derived from this software without specific prior written permission.
94 92
 				</ul>
95 93
 				<p>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p>
96
-
94
+				<p>QR codes are generated by a slightly modified <a href="https://github.com/papnkukn/qrcode-svg">qrcode-svg</a> which is provided under the following license.</p>
95
+				<p>The MIT License (MIT)</p>
96
+				<p>Copyright (c) 2016 papnkukn</p>
97
+				<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>
98
+				<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>
99
+				<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
100
+				<p>The word "QR Code" is <a href="http://www.denso-wave.com/qrcode/faqpatent-e.html">registered trademark of DENSO WAVE INCORPORATED</a>.</p>
97 101
 			</div>
98 102
 		</div>
99 103
 	</body>

BIN
src/resources/icons/appicons/logo.icns View File


BIN
src/resources/icons/appicons/logo.ico View File


+ 84
- 18
src/resources/icons/appicons/logo.svg View File

@@ -1,18 +1,84 @@
1
-<?xml version="1.0" standalone="yes"?>
2
-<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3
-<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewbox="0 0 512 512" width="512px" height="512px">
4
-	<title>Typica application icon</title>
5
-	<defs>
6
-		<radialGradient id="orangeradial" gradientUnits="userSpaceOnUse" cx="288" cy="288" r="320" fx="320" fy="320" spreadMethod="pad">
7
-			<stop offset="0%" stop-color="orangered" />
8
-			<stop offset="100%" stop-color="white" />
9
-		</radialGradient>
10
-		<radialGradient id="flame" gradientUnits="userSpaceOnUse" cx="192" cy="320" r="224" fx="224" fy="352" spreadmethod="pad">
11
-			<stop offset="65%" stop-color="royalblue" />
12
-			<stop offset="100%" stop-color="aliceblue" />
13
-		</radialGradient>
14
-	</defs>
15
-	<path d="M416 32 L416 480 L32 480 L32 480 L32 96 A64 64 0 0 1 96 32 Z" stroke="orangered" stroke-width="1" fill="url(#orangeradial)" />
16
-	<path d="M32 384 L160 128 L480 288 L384 480 L32 480 Z" stroke="orangered" stroke-width="1" fill="aliceblue" />
17
-	<path d="M64 352 C64 448 352 448 352 352 Q352 320 288 256 C224 192 224 160 288 96 C192 96 64 288 64 352" stroke="royalblue" fill="url(#flame)" stroke-width="1" />
18
-</svg>
1
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
+
4
+<svg
5
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
6
+   xmlns:cc="http://creativecommons.org/ns#"
7
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8
+   xmlns:svg="http://www.w3.org/2000/svg"
9
+   xmlns="http://www.w3.org/2000/svg"
10
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12
+   width="205.03497mm"
13
+   height="205.03497mm"
14
+   viewBox="0 0 726.50187 726.50187"
15
+   id="svg2"
16
+   version="1.1"
17
+   inkscape:version="0.91 r13725"
18
+   sodipodi:docname="typica2logo.svg">
19
+  <defs
20
+     id="defs4" />
21
+  <sodipodi:namedview
22
+     id="base"
23
+     pagecolor="#ffffff"
24
+     bordercolor="#666666"
25
+     borderopacity="1.0"
26
+     inkscape:pageopacity="0.0"
27
+     inkscape:pageshadow="2"
28
+     inkscape:zoom="0.5"
29
+     inkscape:cx="437.07058"
30
+     inkscape:cy="264.99486"
31
+     inkscape:document-units="px"
32
+     inkscape:current-layer="layer2"
33
+     showgrid="true"
34
+     fit-margin-top="0"
35
+     fit-margin-left="0"
36
+     fit-margin-right="0"
37
+     fit-margin-bottom="0"
38
+     inkscape:window-width="1920"
39
+     inkscape:window-height="1020"
40
+     inkscape:window-x="-5"
41
+     inkscape:window-y="0"
42
+     inkscape:window-maximized="1">
43
+    <inkscape:grid
44
+       type="xygrid"
45
+       id="grid4667" />
46
+  </sodipodi:namedview>
47
+  <metadata
48
+     id="metadata7">
49
+    <rdf:RDF>
50
+      <cc:Work
51
+         rdf:about="">
52
+        <dc:format>image/svg+xml</dc:format>
53
+        <dc:type
54
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
55
+        <dc:title></dc:title>
56
+      </cc:Work>
57
+    </rdf:RDF>
58
+  </metadata>
59
+  <g
60
+     inkscape:label="Layer 1"
61
+     inkscape:groupmode="layer"
62
+     id="layer1"
63
+     style="display:inline"
64
+     transform="translate(-20.191517,-307.72555)" />
65
+  <g
66
+     inkscape:groupmode="layer"
67
+     id="layer2"
68
+     inkscape:label="Layer 2"
69
+     style="display:inline;opacity:1"
70
+     transform="translate(-20.191517,-307.72555)">
71
+    <path
72
+       style="fill:#7f6a0d;fill-opacity:1;stroke:#8a7127;stroke-width:1.00999999;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0"
73
+       d="M 20.76606,1032.3588 C 22.14203,896.38392 73.17102,731.09727 146.2537,611.20266 216.74295,509.15107 235.78235,504.35496 305.5479,447.39049 340.61147,417.6994 379.33066,338.50538 388.03248,324.7659 c 1.27482,64.16895 -12.66237,162.28087 -17.0384,226.23928 -7.89434,68.25736 -12.71934,137.33988 -28.45455,204.33381 -12.61427,42.79597 -18.00679,31.23433 -77.25862,35.52947 C 171.58481,865.99877 79.3506,933.87182 20.76606,1032.3588 Z"
74
+       id="path3462"
75
+       inkscape:connector-curvature="0"
76
+       sodipodi:nodetypes="cccccccc" />
77
+    <path
78
+       style="fill:#053d1a;fill-opacity:1;stroke:#679b27;stroke-width:2.05999994;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0"
79
+       d="m 21.10538,1031.605 c 54.0778,-104.01153 121.63419,-191.53118 229.58628,-244.0922 100.53161,-42.70356 222.64766,-3.87115 320.77056,37.23617 54.94778,23.8555 114.20568,40.7637 174.57878,39.5697 -73.2709,59.97445 -165.88148,85.56725 -254.68528,112.7226 -69.99517,19.70993 -142.97415,31.19113 -215.74856,24.48983 -85.44247,-3.3499 -174.52237,-4.7892 -254.50178,30.0739 z"
80
+       id="path3464"
81
+       inkscape:connector-curvature="0"
82
+       sodipodi:nodetypes="ccccccc" />
83
+  </g>
84
+</svg>

BIN
src/resources/icons/appicons/logo16.ico View File


BIN
src/resources/icons/appicons/logo16.png View File


BIN
src/resources/icons/appicons/logo24.png View File


BIN
src/resources/icons/appicons/logo256.ico View File


BIN
src/resources/icons/appicons/logo32.ico View File


BIN
src/resources/icons/appicons/logo48.ico View File


BIN
src/resources/icons/appicons/logo48.png View File


BIN
src/resources/icons/appicons/logo96.png View File


+ 38
- 16
src/scale.cpp View File

@@ -1,5 +1,5 @@
1
-/*1008:*/
2
-#line 131 "./scales.w"
1
+/*1060:*/
2
+#line 135 "./scales.w"
3 3
 
4 4
 #include "scale.h"
5 5
 #include <QStringList> 
@@ -10,8 +10,8 @@ QextSerialPort(port,QextSerialPort::EventDriven)
10 10
 connect(this,SIGNAL(readyRead()),this,SLOT(dataAvailable()));
11 11
 }
12 12
 
13
-/*:1008*//*1009:*/
14
-#line 149 "./scales.w"
13
+/*:1060*//*1061:*/
14
+#line 153 "./scales.w"
15 15
 
16 16
 void SerialScale::dataAvailable()
17 17
 {
@@ -24,8 +24,8 @@ responseBuffer.clear();
24 24
 }
25 25
 else
26 26
 {
27
-/*1010:*/
28
-#line 189 "./scales.w"
27
+/*1062:*/
28
+#line 193 "./scales.w"
29 29
 
30 30
 QStringList responseParts= QString(responseBuffer.simplified()).split(' ');
31 31
 if(responseParts.size()> 2)
@@ -35,34 +35,34 @@ responseParts.replace(0,QString("-%1").arg(responseParts[0]));
35 35
 }
36 36
 double weight= responseParts[0].toDouble();
37 37
 Units::Unit unit= Units::Unitless;
38
-if(responseParts[1]=="lb")
38
+if(responseParts[1].compare("lb",Qt::CaseInsensitive)==0)
39 39
 {
40 40
 unit= Units::Pound;
41 41
 }
42
-else if(responseParts[1]=="kg")
42
+else if(responseParts[1].compare("kg",Qt::CaseInsensitive)==0)
43 43
 {
44 44
 unit= Units::Kilogram;
45 45
 }
46
-else if(responseParts[1]=="g")
46
+else if(responseParts[1].compare("g",Qt::CaseInsensitive)==0)
47 47
 {
48 48
 unit= Units::Gram;
49 49
 }
50
-else if(responseParts[1]=="oz")
50
+else if(responseParts[1].compare("oz",Qt::CaseInsensitive)==0)
51 51
 {
52 52
 unit= Units::Ounce;
53 53
 }
54 54
 emit newMeasurement(weight,unit);
55 55
 
56
-/*:1010*/
57
-#line 161 "./scales.w"
56
+/*:1062*/
57
+#line 165 "./scales.w"
58 58
 
59 59
 responseBuffer.clear();
60 60
 }
61 61
 }
62 62
 }
63 63
 
64
-/*:1009*//*1011:*/
65
-#line 220 "./scales.w"
64
+/*:1061*//*1063:*/
65
+#line 224 "./scales.w"
66 66
 
67 67
 void SerialScale::tare()
68 68
 {
@@ -71,7 +71,29 @@ write("!KT\x0D");
71 71
 
72 72
 void SerialScale::weigh()
73 73
 {
74
-write("!KP\x0D");
74
+
75
+write(weighCommand+commandTerminator);
76
+}
77
+
78
+void SerialScale::setWeighCommand(const QString&command)
79
+{
80
+weighCommand= command.toAscii();
81
+}
82
+
83
+void SerialScale::setCommandTerminator(const QString&terminator)
84
+{
85
+if(terminator=="CRLF")
86
+{
87
+commandTerminator= "\x0D\x0A";
88
+}
89
+else if(terminator=="CR")
90
+{
91
+commandTerminator= "\x0D";
92
+}
93
+else if(terminator=="LF")
94
+{
95
+commandTerminator= "\x0A";
96
+}
75 97
 }
76 98
 
77
-/*:1011*/
99
+/*:1063*/

+ 6
- 2
src/scale.h View File

@@ -1,4 +1,4 @@
1
-/*1007:*/
1
+/*1059:*/
2 2
 #line 103 "./scales.w"
3 3
 
4 4
 #ifndef TypicaScaleInclude
@@ -15,14 +15,18 @@ SerialScale(const QString&port);
15 15
 public slots:
16 16
 void tare();
17 17
 void weigh();
18
+void setWeighCommand(const QString&command);
19
+void setCommandTerminator(const QString&terminator);
18 20
 signals:
19 21
 void newMeasurement(double weight,Units::Unit unit);
20 22
 private slots:
21 23
 void dataAvailable();
22 24
 private:
23 25
 QByteArray responseBuffer;
26
+QByteArray weighCommand;
27
+QByteArray commandTerminator;
24 28
 };
25 29
 
26 30
 #endif
27 31
 
28
-/*:1007*/
32
+/*:1059*/

+ 68
- 6
src/scales.w View File

@@ -115,12 +115,16 @@ class SerialScale : public QextSerialPort
115 115
 	public slots:
116 116
 		void tare();
117 117
 		void weigh();
118
+		void setWeighCommand(const QString &command);
119
+		void setCommandTerminator(const QString &terminator);
118 120
 	signals:
119 121
 		void newMeasurement(double weight, Units::Unit unit);
120 122
 	private slots:
121 123
 		void dataAvailable();
122 124
 	private:
123 125
 		QByteArray responseBuffer;
126
+		QByteArray weighCommand;
127
+		QByteArray commandTerminator;
124 128
 };
125 129
 
126 130
 #endif
@@ -195,19 +199,19 @@ if(responseParts.size() > 2)
195 199
 }
196 200
 double weight = responseParts[0].toDouble();
197 201
 Units::Unit unit = Units::Unitless;
198
-if(responseParts[1] == "lb")
202
+if(responseParts[1].compare("lb", Qt::CaseInsensitive) == 0)
199 203
 {
200 204
 	unit = Units::Pound;
201 205
 }
202
-else if(responseParts[1] == "kg")
206
+else if(responseParts[1].compare("kg", Qt::CaseInsensitive) == 0)
203 207
 {
204 208
 	unit = Units::Kilogram;
205 209
 }
206
-else if(responseParts[1] == "g")
210
+else if(responseParts[1].compare("g", Qt::CaseInsensitive) == 0)
207 211
 {
208 212
 	unit = Units::Gram;
209 213
 }
210
-else if(responseParts[1] == "oz")
214
+else if(responseParts[1].compare("oz", Qt::CaseInsensitive) == 0)
211 215
 {
212 216
 	unit = Units::Ounce;
213 217
 }
@@ -225,7 +229,29 @@ void SerialScale::tare()
225 229
 
226 230
 void SerialScale::weigh()
227 231
 {
228
-	write("!KP\x0D");
232
+	//write("!KP\x0D");
233
+	write(weighCommand + commandTerminator);
234
+}
235
+
236
+void SerialScale::setWeighCommand(const QString &command)
237
+{
238
+	weighCommand = command.toAscii();
239
+}
240
+
241
+void SerialScale::setCommandTerminator(const QString &terminator)
242
+{
243
+	if(terminator == "CRLF")
244
+	{
245
+		commandTerminator = "\x0D\x0A";
246
+	}
247
+	else if(terminator == "CR")
248
+	{
249
+		commandTerminator = "\x0D";
250
+	}
251
+	else if(terminator == "LF")
252
+	{
253
+		commandTerminator = "\x0A";
254
+	}
229 255
 }
230 256
 
231 257
 @ This must be available to the host environment.
@@ -380,12 +406,16 @@ class SerialScaleConfWidget : public BasicDeviceConfigurationWidget
380 406
 		void updateParity(int index);
381 407
 		void updateFlowControl(int index);
382 408
 		void updateStopBits(int index);
409
+		void updateWeighCommand(const QString &command);
410
+		void updateCommandTerminator(const QString &terminator);
383 411
 	private:
384 412
 		PortSelector *port;
385 413
 		BaudSelector *baud;
386 414
 		ParitySelector *parity;
387 415
 		FlowSelector *flow;
388 416
 		StopSelector *stop;
417
+		QLineEdit *weighcommand;
418
+		QComboBox *commandterminator;
389 419
 };
390 420
 
391 421
 @ This is very similar to other configuration widgets.
@@ -395,7 +425,8 @@ SerialScaleConfWidget::SerialScaleConfWidget(DeviceTreeModel *model,
395 425
                                              const QModelIndex &index)
396 426
 : BasicDeviceConfigurationWidget(model, index),
397 427
   port(new PortSelector), baud(new BaudSelector), parity(new ParitySelector),
398
-  flow(new FlowSelector), stop(new StopSelector)
428
+  flow(new FlowSelector), stop(new StopSelector),
429
+  weighcommand(new QLineEdit("!KP")), commandterminator(new QComboBox)
399 430
 {
400 431
 	QFormLayout *layout = new QFormLayout;
401 432
 	layout->addRow(tr("Port:"), port);
@@ -415,6 +446,16 @@ SerialScaleConfWidget::SerialScaleConfWidget(DeviceTreeModel *model,
415 446
 	layout->addRow(tr("Stop Bits:"), stop);
416 447
 	connect(stop, SIGNAL(currentIndexChanged(int)),
417 448
 	        this, SLOT(updateStopBits(int)));
449
+	layout->addRow(tr("Weigh Command:"), weighcommand);
450
+	connect(weighcommand, SIGNAL(textChanged(QString)),
451
+	        this, SLOT(updateWeighCommand(QString)));
452
+	commandterminator->addItem("CRLF");
453
+	commandterminator->addItem("CR");
454
+	commandterminator->addItem("LF");
455
+	layout->addRow(tr("Command Terminator:"), commandterminator);
456
+	connect(commandterminator, SIGNAL(currentIndexChanged(QString)),
457
+	        this, SLOT(updateCommandTerminator(QString)));
458
+	
418 459
 	@<Get device configuration data for current node@>@;
419 460
 	for(int i = 0; i < configData.size(); i++)
420 461
 	{
@@ -448,12 +489,23 @@ SerialScaleConfWidget::SerialScaleConfWidget(DeviceTreeModel *model,
448 489
 		{
449 490
 			stop->setCurrentIndex(stop->findData(node.attribute("value")));
450 491
 		}
492
+		else if(node.attribute("name") == "weighcommand")
493
+		{
494
+			weighcommand->setText(node.attribute("value"));
495
+		}
496
+		else if(node.attribute("name") == "commandterminator")
497
+		{
498
+			commandterminator->setCurrentIndex(
499
+				commandterminator->findText(node.attribute("value")));
500
+		}
451 501
 	}
452 502
 	updatePort(port->currentText());
453 503
 	updateBaudRate(baud->currentText());
454 504
 	updateParity(parity->currentIndex());
455 505
 	updateFlowControl(flow->currentIndex());
456 506
 	updateStopBits(stop->currentIndex());
507
+	updateWeighCommand(weighcommand->text());
508
+	updateCommandTerminator(commandterminator->currentText());
457 509
 	setLayout(layout);
458 510
 }
459 511
 
@@ -485,6 +537,16 @@ void SerialScaleConfWidget::updateStopBits(int index)
485 537
 	updateAttribute("stopbits", stop->itemData(index).toString());
486 538
 }
487 539
 
540
+void SerialScaleConfWidget::updateWeighCommand(const QString &command)
541
+{
542
+	updateAttribute("weighcommand", command);
543
+}
544
+
545
+void SerialScaleConfWidget::updateCommandTerminator(const QString &terminator)
546
+{
547
+	updateAttribute("commandterminator", terminator);
548
+}
549
+
488 550
 @ The configuration widget is registered with the configuration system.
489 551
 
490 552
 @<Register device configuration widgets@>=

+ 220
- 0
src/thresholdannotation.w View File

@@ -0,0 +1,220 @@
1
+@** Threshold Annotations.
2
+
3
+\noindent Value annotations are fine for cases where we want to capture the
4
+fact that a control series has changed to some specific value, but there are
5
+times when it is more useful to know when a data series has passed through some
6
+value in a particular direction even if the exact value of interest is never
7
+directly recorded. For example, it would be possible to automatically mark a
8
+point near turnaround by watching for a rate of temperature change ascending
9
+through 0. This could also be set up to match range timers and mark events of
10
+interest that begin at consistent temperatures.
11
+
12
+As usual, this is a feature that must be configured on a per roaster basis.
13
+
14
+@<Class declarations@>=
15
+class ThresholdAnnotationConfWidget : public BasicDeviceConfigurationWidget
16
+{
17
+    Q_OBJECT
18
+    public:
19
+        Q_INVOKABLE ThresholdAnnotationConfWidget(DeviceTreeModel *model,
20
+                                                  const QModelIndex &index);
21
+    private slots:
22
+        void updateSourceColumn(const QString &source);
23
+        void updateThreshold(double value);
24
+        void updateDirection(int index);
25
+        void updateAnnotation(const QString &note);
26
+};
27
+
28
+@ The configuration widget needs to provide fields for determining which data
29
+series should be used to generate the annotation, the value that the
30
+|ThresholdDetector| should use as its trigger, the direction this should fire
31
+on, and the text of the annotation.
32
+
33
+@<ThresholdAnnotationConfWidget implementation@>=
34
+ThresholdAnnotationConfWidget::ThresholdAnnotationConfWidget(DeviceTreeModel *model,
35
+                                                             const QModelIndex &index)
36
+: BasicDeviceConfigurationWidget(model, index)
37
+{
38
+    QFormLayout *layout = new QFormLayout;
39
+    QLineEdit *source = new QLineEdit;
40
+    layout->addRow(tr("Source column name:"), source);
41
+    QDoubleSpinBox *value = new QDoubleSpinBox;
42
+    value->setMinimum(-9999.99);
43
+    value->setMaximum(9999.99);
44
+    value->setDecimals(2);
45
+    layout->addRow(tr("Threshold value:"), value);
46
+    QComboBox *direction = new QComboBox;
47
+    direction->addItem(tr("Ascending"));
48
+    direction->addItem(tr("Descending"));
49
+    layout->addRow(tr("Direction:"), direction);
50
+    QLineEdit *annotation = new QLineEdit;
51
+    layout->addRow(tr("Annotation:"), annotation);
52
+    @<Get device configuration data for current node@>@;
53
+    for(int i = 0; i < configData.size(); i++)
54
+    {
55
+        node = configData.at(i).toElement();
56
+        if(node.attribute("name") == "source")
57
+        {
58
+            source->setText(node.attribute("value"));
59
+        }
60
+        else if(node.attribute("name") == "value")
61
+        {
62
+            value->setValue(node.attribute("value").toDouble());
63
+        }
64
+        else if(node.attribute("name") == "direction")
65
+        {
66
+            direction->setCurrentIndex(node.attribute("value").toInt());
67
+        }
68
+        else if(node.attribute("name") == "annotation")
69
+        {
70
+            annotation->setText(node.attribute("value"));
71
+        }
72
+    }
73
+    updateSourceColumn(source->text());
74
+    updateThreshold(value->value());
75
+    updateDirection(direction->currentIndex());
76
+    updateAnnotation(annotation->text());
77
+    connect(source, SIGNAL(textEdited(QString)), this, SLOT(updateSourceColumn(QString)));
78
+    connect(value, SIGNAL(valueChanged(double)), this, SLOT(updateThreshold(double)));
79
+    connect(direction, SIGNAL(currentIndexChanged(int)), this, SLOT(updateDirection(int)));
80
+    connect(annotation, SIGNAL(textEdited(QString)), this, SLOT(updateAnnotation(QString)));
81
+    setLayout(layout);
82
+}
83
+
84
+@ Configuration of the model is done as usual.
85
+
86
+@<ThresholdAnnotationConfWidget implementation@>=
87
+void ThresholdAnnotationConfWidget::updateSourceColumn(const QString &source)
88
+{
89
+    updateAttribute("source", source);
90
+}
91
+
92
+void ThresholdAnnotationConfWidget::updateThreshold(double value)
93
+{
94
+    updateAttribute("value", QString("%1").arg(value));
95
+}
96
+
97
+void ThresholdAnnotationConfWidget::updateDirection(int direction)
98
+{
99
+    updateAttribute("direction", QString("%1").arg(direction));
100
+}
101
+
102
+void ThresholdAnnotationConfWidget::updateAnnotation(const QString &annotation)
103
+{
104
+    updateAttribute("annotation", annotation);
105
+}
106
+
107
+@ The configurationwidget is registered with the configuration system as usual.
108
+
109
+@<Register device configuration widgets@>=
110
+app.registerDeviceConfigurationWidget("thresholdannotation",
111
+                                      ThresholdAnnotationConfWidget::staticMetaObject);
112
+
113
+@ A NodeInserter makes the configuration available.
114
+
115
+@<Add annotation control node inserters@>=
116
+NodeInserter *thresholdAnnotationInserter = new NodeInserter(tr("Threshold Annotation"),
117
+                                                             tr("Threshold Annotation"),
118
+                                                             "thresholdannotation");
119
+annotationMenu->addAction(thresholdAnnotationInserter);
120
+connect(thresholdAnnotationInserter, SIGNAL(triggered(QString, QString)),
121
+        this, SLOT(insertChildNode(QString, QString)));
122
+
123
+@ While we could use |ThresholdDetector| in the configuration directly, it is
124
+easier to provide another class with the same interface as |AnnotationButton|
125
+to leverage existing code for handling these.
126
+
127
+@<Class declarations@>=
128
+class Annotator : public QObject
129
+{@t\1@>@/
130
+    Q_OBJECT@;
131
+    QString note;
132
+    int tc;
133
+    int ac;
134
+    QTimer t;
135
+    public:
136
+        Annotator(const QString &text);@/
137
+    @t\4@>public slots@t\kern-3pt@>:@/
138
+        void setAnnotation(const QString &annotation);
139
+        void setTemperatureColumn(int tempcolumn);
140
+        void setAnnotationColumn(int annotationcolumn);
141
+        void annotate();
142
+    private slots:
143
+        void catchTimer();
144
+    signals:@/
145
+        void annotation(QString annotation, int tempcolumn, int notecolumn);@t\2@>@/
146
+}@t\kern-3pt@>;
147
+
148
+@ To use this class with a |ThresholdDetector|, simply connect the
149
+|timeForValue()| signal to the |annotate()| slot and use the existing
150
+|AnnotationButton| code to keep the columns up to date.
151
+
152
+@<Annotator implementation@>=
153
+Annotator::Annotator(const QString &text) : QObject(NULL), note(text)
154
+{
155
+    t.setInterval(0);
156
+    t.setSingleShot(true);
157
+    connect(&t, SIGNAL(timeout()), this, SLOT(catchTimer()));
158
+}
159
+
160
+void Annotator::setAnnotation(const QString &annotation)
161
+{
162
+    note = annotation;
163
+}
164
+
165
+void Annotator::setTemperatureColumn(int tempcolumn)
166
+{
167
+    tc = tempcolumn;
168
+}
169
+
170
+void Annotator::setAnnotationColumn(int annotationcolumn)
171
+{
172
+    ac = annotationcolumn;
173
+}
174
+
175
+@ When connecting a |ThresholdDetector| to an |Annotator| directly, the
176
+annotation can be recorded before the measurement reaches the log. The result
177
+of this is that the annotation appears with the measurement immediately before
178
+the one it should appear next to. To solve this, the annotation is delayed
179
+until the next iteration of the event loop.
180
+
181
+@<Annotator implementation@>=
182
+void Annotator::catchTimer()
183
+{
184
+    emit annotation(note, tc, ac);
185
+}
186
+
187
+void Annotator::annotate()
188
+{
189
+    t.start();
190
+}
191
+
192
+@ It must be possible to create these from a script.
193
+
194
+@<Function prototypes for scripting@>=
195
+QScriptValue constructAnnotator(QScriptContext *context,
196
+                                QScriptEngine *engine);
197
+void setAnnotatorProperties(QScriptValue value, QScriptEngine *engine);
198
+
199
+@ The engine is informed of the constructor.
200
+
201
+@<Set up the scripting engine@>=
202
+constructor = engine->newFunction(constructAnnotator);
203
+value = engine->newQMetaObject(&Annotator::staticMetaObject, constructor);
204
+engine->globalObject().setProperty("Annotator", value);
205
+
206
+@ The implementation is trivial.
207
+
208
+@<Functions for scripting@>=
209
+QScriptValue constructAnnotator(QScriptContext *context, QScriptEngine *engine)
210
+{
211
+    QScriptValue object = engine->newQObject(new Annotator(argument<QString>(0, context)));
212
+    setAnnotatorProperties(object, engine);
213
+    return object;
214
+}
215
+
216
+void setAnnotatorProperties(QScriptValue value, QScriptEngine *engine)
217
+{
218
+    setQObjectProperties(value, engine);
219
+}
220
+

+ 4671
- 2559
src/typica.cpp
File diff suppressed because it is too large
View File


+ 7
- 7
src/typica.rc View File

@@ -1,7 +1,7 @@
1 1
 #include <winver.h>
2 2
 VS_VERSION_INFO	VERSIONINFO
3
-FILEVERSION 	1,7,0,0
4
-PRODUCTVERSION 	1,7,0,0
3
+FILEVERSION 	1,8,0,0
4
+PRODUCTVERSION 	1,8,0,0
5 5
 FILEFLAGSMASK	0x3fL
6 6
 #ifdef _DEBUG
7 7
 	FILEFLAGS	VS_FF_DEBUG
@@ -16,13 +16,13 @@ BEGIN
16 16
 		BLOCK "040904b0"
17 17
 		BEGIN
18 18
 			VALUE "CompanyName", "Wilson's Coffee & Tea\0"
19
-			VALUE "FileDescription", "Typica 1.7.0\0"
20
-			VALUE "FileVersion", "1.7.0\0"
19
+			VALUE "FileDescription", "Typica 1.8.0\0"
20
+			VALUE "FileVersion", "1.8.0\0"
21 21
 			VALUE "InternalName", "Typica\0"
22
-			VALUE "LegalCopyright", "Copyright 2007-2016 Neal Evan Wilson\0"
22
+			VALUE "LegalCopyright", "Copyright 2007-2017 Neal Evan Wilson\0"
23 23
 			VALUE "OriginalFilename", "Typica.exe\0"
24 24
 			VALUE "ProductName", "Typica\0"
25
-			VALUE "ProductVersion", "1.7.0\0"
25
+			VALUE "ProductVersion", "1.8.0\0"
26 26
 		END
27 27
 	END
28 28
 	BLOCK "VarFileInfo"
@@ -31,4 +31,4 @@ BEGIN
31 31
 	END
32 32
 END
33 33
 
34
-IDI_ICON1	ICON	DISCARDABLE	"resources/icons/appicons/logo256.ico"
34
+IDI_ICON1	ICON	DISCARDABLE	"resources/icons/appicons/logo.ico"

+ 326
- 25
src/typica.w View File

@@ -22,8 +22,8 @@
22 22
 \mark{\noexpand\nullsec0{A Note on Notation}}
23 23
 \def\pn{Typica}
24 24
 \def\filebase{typica}
25
-\def\version{1.7.0 \number\year-\number\month-\number\day}
26
-\def\years{2007--2016}
25
+\def\version{1.8.0 \number\year-\number\month-\number\day}
26
+\def\years{2007--2017}
27 27
 \def\title{\pn{} (Version \version)}
28 28
 \newskip\dangerskipb
29 29
 \newskip\dangerskip
@@ -609,6 +609,9 @@ generated file empty.
609 609
 @<SerialScaleConfWidget implementation@>@/
610 610
 @<ValueAnnotation implementation@>@/
611 611
 @<ValueAnnotationConfWidget implementation@>@/
612
+@<ModbusNG implementation@>@/
613
+@<ThresholdAnnotationConfWidget implementation@>@/
614
+@<Annotator implementation@>@/
612 615
 
613 616
 @ A few headers are required for various parts of \pn{}. These allow the use of
614 617
 various Qt modules.
@@ -807,15 +810,25 @@ The first of these is |QObject|.
807 810
 
808 811
 @<Function prototypes for scripting@>=
809 812
 void setQObjectProperties(QScriptValue value, QScriptEngine *engine);
813
+QScriptValue QObject_setProperty(QScriptContext *context, QScriptEngine *engine);
810 814
 
811
-@ As there are no properties that need to be set for this class and as this
812
-class does not inherit any other class, nothing needs to be done in this method.
813
-It will, however, be called by subclasses in case this changes in the future.
815
+@ Attaching properties to a |QScriptValue| that wraps a |QObject| does not
816
+create a dynamic property on the underlying |QObject| by default. This can
817
+cause issues with certain interactions between script and native code. Rather
818
+than change every wrapper, we can instead expose a |setProperty()| method.
814 819
 
815 820
 @<Functions for scripting@>=
816
-void setQObjectProperties(QScriptValue, QScriptEngine *)
821
+void setQObjectProperties(QScriptValue value, QScriptEngine *engine)
817 822
 {
818
-    /* Nothing needs to be done here. */
823
+    value.setProperty("setProperty", engine->newFunction(QObject_setProperty));
824
+}
825
+
826
+QScriptValue QObject_setProperty(QScriptContext *context, QScriptEngine *)
827
+{
828
+	QObject *self = getself<QObject *>(context);
829
+	self->setProperty(argument<QString>(0, context).toUtf8().constData(),
830
+	                  argument<QVariant>(1, context));
831
+    return QScriptValue();
819 832
 }
820 833
 
821 834
 @ The same can be done for |QPaintDevice| and |QLayoutItem|.
@@ -837,6 +850,41 @@ void setQLayoutItemProperties(QScriptValue, QScriptEngine *)
837 850
     /* Nothing needs to be done here. */
838 851
 }
839 852
 
853
+@* Timers.
854
+
855
+\noindent Some features in Typica require access to functionality similar to
856
+what |QTimer| provides from the host environment. This includes allowing
857
+script devices to periodically poll connected hardware and allowing a safety
858
+delay on profile translation.
859
+
860
+<@Function prototypes for scripting@>=
861
+void setQTimerProperties(QScriptValue value, QScriptEngine *engine);
862
+QScriptValue constructQTimer(QScriptContext *context, QScriptEngine *engine);
863
+
864
+@ The host environment is informed of the constructor.
865
+
866
+@<Set up the scripting engine@>=
867
+constructor = engine->newFunction(constructQTimer);
868
+value = engine->newQMetaObject(&QTimer::staticMetaObject, constructor);
869
+engine->globalObject().setProperty("Timer", value);
870
+
871
+@ Everything that we are interested in here is a signal, slot, or property so
872
+there is little else to do.
873
+
874
+@<Functions for scripting@>=
875
+void setQTimerProperties(QScriptValue value, QScriptEngine *engine)
876
+{
877
+	setQObjectProperties(value, engine);
878
+}
879
+
880
+QScriptValue constructQTimer(QScriptContext *, QScriptEngine *engine)
881
+{
882
+	QScriptValue object = engine->newQObject(new QTimer);
883
+	setQTimerProperties(object, engine);
884
+	return object;
885
+}
886
+
887
+
840 888
 @* Scripting QWidget.
841 889
 
842 890
 \noindent The first interesting class in this hierarchy is |QWidget|. This is
@@ -991,6 +1039,15 @@ considered depreciated.
991 1039
 Version 1.6 adds a new property for handling the |windowModified| property
992 1040
 such that an appropriate prompt is provided to confirm or cancel close events.
993 1041
 
1042
+Version 1.8 adds a new |setupFinished()| slot which is called after the
1043
+initial |show()| at the end of window creation. This emits a |windowReady()|
1044
+signal. Scripts can connect to this signal to perform tasks that must happen
1045
+after the window has fully finished opening. The initial use for this is
1046
+validating that all required configuration has been performed for a given
1047
+window to be useful and, if not, immediately closing that. Without this, a
1048
+call to |close()| in the script is reversed when the function creating the
1049
+window calls |show()|.
1050
+
994 1051
 @<Class declarations@>=
995 1052
 class ScriptQMainWindow : public QMainWindow@/
996 1053
 {@t\1@>@/
@@ -1004,12 +1061,14 @@ class ScriptQMainWindow : public QMainWindow@/
1004 1061
         void saveSizeAndPosition(const QString &key);
1005 1062
         void restoreSizeAndPosition(const QString &key);
1006 1063
         void displayStatus(const QString &message = QString());
1007
-        void setClosePrompt(QString prompt);@/
1064
+        void setClosePrompt(QString prompt);
1065
+        void setupFinished();@/
1066
+    signals:@/
1067
+        void aboutToClose(void);
1068
+        void windowReady(void);@/
1008 1069
     protected:@/
1009 1070
         void closeEvent(QCloseEvent *event);
1010 1071
         void showEvent(QShowEvent *event);@/
1011
-    signals:@/
1012
-        void aboutToClose(void);@/
1013 1072
     private:@/
1014 1073
         QString cprompt;@t\2@>@/
1015 1074
 }@t\kern-3pt@>;
@@ -1020,7 +1079,14 @@ class ScriptQMainWindow : public QMainWindow@/
1020 1079
 ScriptQMainWindow::ScriptQMainWindow()@+: QMainWindow(NULL),
1021 1080
     cprompt(tr("Closing this window may result in loss of data. Continue?"))@/
1022 1081
 {
1023
-    /* Nothing needs to be done here. */
1082
+    if(!AppInstance->databaseConnected())
1083
+    {
1084
+	    statusBar()->addWidget(new QLabel(tr("Not connected to database")));
1085
+    }
1086
+    else
1087
+    {
1088
+	    statusBar()->addWidget(new UserLabel);
1089
+    }
1024 1090
 }
1025 1091
 
1026 1092
 void ScriptQMainWindow::saveSizeAndPosition(const QString &key)
@@ -1070,6 +1136,11 @@ void ScriptQMainWindow::show()
1070 1136
     QMainWindow::show();
1071 1137
 }
1072 1138
 
1139
+void ScriptQMainWindow::setupFinished()
1140
+{
1141
+	emit windowReady();
1142
+}
1143
+
1073 1144
 @ When a close event occurs, we check the |windowModified| property to
1074 1145
 determine if closing the window could result in loss of data. If this is
1075 1146
 true, we allow the event to be cancelled. Otherwise, a signal is emitted which
@@ -4329,6 +4400,9 @@ Starting with version 1.4, we check for a command line option with the path to
4329 4400
 the configuration file and load that instead of prompting for the information
4330 4401
 if possible.
4331 4402
 
4403
+Starting with version 1.8, if there is not a -c argument, Typica will first
4404
+search a small number of locations relative to the executable.
4405
+
4332 4406
 @<Load the application configuration@>=
4333 4407
 QStringList arguments = QCoreApplication::arguments();
4334 4408
 int position = arguments.indexOf("-c");
@@ -4339,6 +4413,16 @@ if(position != -1)
4339 4413
     {
4340 4414
         filename = arguments.at(position + 1);
4341 4415
     }
4416
+} else {
4417
+	QDir checkPath(QCoreApplication::applicationDirPath() + "/../config/");
4418
+	if(checkPath.exists("config.xml")) {
4419
+		filename = checkPath.filePath("config.xml");
4420
+	} else {
4421
+		checkPath = QDir(QCoreApplication::applicationDirPath() + "/config/");
4422
+		if(checkPath.exists("config.xml")) {
4423
+			filename  = checkPath.filePath("config.xml");
4424
+		}
4425
+	}
4342 4426
 }
4343 4427
 if(filename.isEmpty())
4344 4428
 {
@@ -4363,6 +4447,8 @@ if(!filename.isEmpty())
4363 4447
     {
4364 4448
         app.configuration()->setContent(&file, true);
4365 4449
     }
4450
+} else {
4451
+	return 1;
4366 4452
 }
4367 4453
 @<Substitute included fragments@>@;
4368 4454
 
@@ -4533,6 +4619,8 @@ void addCalendarToLayout(QDomElement element, QStack<QWidget *> *widgetStack,
4533 4619
                          QStack<QLayout *> *layoutStack);
4534 4620
 void addSpinBoxToLayout(QDomElement element, QStack<QWidget *> *widgetStack,
4535 4621
                         QStack<QLayout *> *layoutStack);
4622
+void addTimeEditToLayout(QDomElement element, QStack<QWidget *> *widgetStack,
4623
+                         QStack<QLayout *> *layoutStack);
4536 4624
 
4537 4625
 @ The functions for creating windows must be made available to the scripting
4538 4626
 engine.
@@ -4641,6 +4729,7 @@ if(element.hasChildNodes())
4641 4729
 }
4642 4730
 @<Insert help menu@>@;
4643 4731
 window->show();
4732
+window->setupFinished();
4644 4733
 
4645 4734
 @ Three element types make sense as top level children of a {\tt <window>}
4646 4735
 element. An element representing a layout element can be used to apply that
@@ -4764,6 +4853,10 @@ while(j < menuItems.count())
4764 4853
         {
4765 4854
             menu->addSeparator();
4766 4855
         }
4856
+        else if(itemElement.tagName() == "plugins")
4857
+        {
4858
+	        @<Process plugin item@>@;
4859
+        }
4767 4860
     }
4768 4861
     j++;
4769 4862
 }
@@ -4877,6 +4970,80 @@ void populateStackedLayout(QDomElement element, QStack<QWidget *> *widgetStack,
4877 4970
     }
4878 4971
 }
4879 4972
 
4973
+@ A common use of stacked layouts is in the creation of tabbed interfaces, but
4974
+there are also many uses in \pn{} where the tabs are not required. Therefore,
4975
+tab bar creation requires a separate XML element.
4976
+
4977
+@<Additional box layout elements@>=
4978
+else if(currentElement.tagName() == "tabbar")
4979
+{
4980
+	addTabBarToLayout(currentElement, widgetStack, layoutStack);
4981
+}
4982
+
4983
+@ The function used to create this follows the usual pattern.
4984
+
4985
+@<Functions for scripting@>=
4986
+void addTabBarToLayout(QDomElement element, QStack<QWidget*> *, QStack<QLayout*> *layoutStack)
4987
+{
4988
+	QBoxLayout *layout = qobject_cast<QBoxLayout *>(layoutStack->top());
4989
+	QTabBar *widget = new QTabBar;
4990
+	layout->addWidget(widget);
4991
+	if(!element.attribute("id").isEmpty())
4992
+	{
4993
+		widget->setObjectName(element.attribute("id"));
4994
+	}
4995
+}
4996
+
4997
+@ Rather than define the tab set in XML, this is left to the host environment.
4998
+This means that some additional scripting support is required.
4999
+
5000
+@<Set up the scripting engine@>=
5001
+constructor = engine->newFunction(constructQTabBar);
5002
+value = engine->newQMetaObject(&QTabBar::staticMetaObject, constructor);
5003
+engine->globalObject().setProperty("QTabBar", value);
5004
+
5005
+@ The constructor is trivial.
5006
+
5007
+@<Functions for scripting@>=
5008
+QScriptValue constructQTabBar(QScriptContext *, QScriptEngine *engine)
5009
+{
5010
+	QScriptValue object = engine->newQObject(new QTabBar);
5011
+	setQTabBarProperties(object, engine);
5012
+	return object;
5013
+}
5014
+
5015
+@ There are many functions that I might want to some day add support for, but
5016
+the immediate need is just creating the tabs in the first place.
5017
+
5018
+@<Functions for scripting@>=
5019
+void setQTabBarProperties(QScriptValue value, QScriptEngine *engine)
5020
+{
5021
+	setQWidgetProperties(value, engine);
5022
+	value.setProperty("addTab", engine->newFunction(QTabBar_addTab));
5023
+}
5024
+
5025
+QScriptValue QTabBar_addTab(QScriptContext *context, QScriptEngine *)
5026
+{
5027
+	QTabBar *self = getself<QTabBar *>(context);
5028
+	if(context->argumentCount() > 0)
5029
+	{
5030
+		self->addTab(argument<QString>(0, context));
5031
+	}
5032
+	else
5033
+	{
5034
+		context->throwError("Incorrect number of arguments passed to "@|
5035
+		                    "QTabBar::addTab().");
5036
+	}
5037
+	return QScriptValue();
5038
+}
5039
+
5040
+@ Function prototypes are needed.
5041
+
5042
+@<Function prototypes for scripting@>=
5043
+QScriptValue constructQTabBar(QScriptContext *context, QScriptEngine *engine);
5044
+void setQTabBarProperties(QScriptValue value, QScriptEngine *engine);
5045
+QScriptValue QTabBar_addTab(QScriptContext *context, QScriptEngine *engine);
5046
+
4880 5047
 @ Using a grid layout is a bit different from using a box layout. Child elements
4881 5048
 with various attributes are required to take full advantage of this layout type.
4882 5049
 All direct children of a grid layout element should be {\tt <row>} elements
@@ -5012,6 +5179,10 @@ void populateBoxLayout(QDomElement element, QStack<QWidget *> *widgetStack,
5012 5179
             {
5013 5180
                 addCalendarToLayout(currentElement, widgetStack, layoutStack);
5014 5181
             }
5182
+            else if(currentElement.tagName() == "timeedit")
5183
+            {
5184
+	            addTimeEditToLayout(currentElement, widgetStack, layoutStack);
5185
+            }
5015 5186
             else if(currentElement.tagName() == "decoration")
5016 5187
             {
5017 5188
                 addDecorationToLayout(currentElement, widgetStack,
@@ -6165,6 +6336,43 @@ QScriptValue QDateTimeEdit_month(QScriptContext *context,
6165 6336
 QScriptValue QDateTimeEdit_year(QScriptContext *context, QScriptEngine *engine);
6166 6337
 QScriptValue QDateTimeEdit_setToCurrentTime(QScriptContext *context, QScriptEngine *engine);
6167 6338
 
6339
+@ Sometimes it can be useful to allow editing a time or duration value without
6340
+a date field. For this, a |QTimeEdit| can be used.
6341
+
6342
+@<Functions for scripting@>=
6343
+void addTimeEditToLayout(QDomElement element, QStack<QWidget *> *,@|
6344
+                         QStack<QLayout *> *layoutStack)
6345
+{
6346
+	QTimeEdit *edit = new QTimeEdit;
6347
+	if(element.hasAttribute("displayFormat"))
6348
+	{
6349
+		edit->setDisplayFormat(element.attribute("displayFormat"));
6350
+	}
6351
+	else
6352
+	{
6353
+		edit->setDisplayFormat("mm:ss.zzz");
6354
+	}
6355
+	if(element.hasAttribute("id"))
6356
+	{
6357
+		edit->setObjectName(element.attribute("id"));
6358
+	}
6359
+	QBoxLayout *layout = qobject_cast<QBoxLayout *>(layoutStack->top());
6360
+	layout->addWidget(edit);
6361
+}
6362
+
6363
+@ Additional properties are added as a |QTimeEdit| is a |QDateTimeEdit|.
6364
+
6365
+@<Functions for scripting@>=
6366
+void setQTimeEditProperties(QScriptValue value, QScriptEngine *engine)
6367
+{
6368
+	setQDateTimeEditProperties(value, engine);
6369
+}
6370
+
6371
+@ A function prototype is needed.
6372
+
6373
+@<Function prototypes for scripting@>=
6374
+void setQTimeEditProperties(QScriptValue value, QScriptEngine *engine);
6375
+
6168 6376
 @ In order to get to objects created from the XML description, it is necessary
6169 6377
 to provide a function that can be called to retrieve children of a given widget.
6170 6378
 When providing such an object to the script, it is necessary to determine the
@@ -6304,6 +6512,14 @@ else if(className == "QSvgWidget")
6304 6512
 {
6305 6513
     setQSvgWidgetProperties(value, engine);
6306 6514
 }
6515
+else if(className == "QTabBar")
6516
+{
6517
+	setQTabBarProperties(value, engine);
6518
+}
6519
+else if(className == "PrinterSelector")
6520
+{
6521
+	setQComboBoxProperties(value, engine);
6522
+}
6307 6523
 
6308 6524
 @ In the list of classes, the SaltTable entry is for a class which does not
6309 6525
 strictly exist on its own. It is, however, useful to provide some custom
@@ -8219,6 +8435,7 @@ class ThresholdDetector : public QObject@/
8219 8435
     signals:@/
8220 8436
         void timeForValue(double);
8221 8437
     private:@/
8438
+        bool previousValueValid;
8222 8439
         double previousValue;
8223 8440
         double threshold;
8224 8441
         EdgeDirection currentDirection;
@@ -8227,12 +8444,19 @@ class ThresholdDetector : public QObject@/
8227 8444
 @ This class emits the time in seconds when a given measurement crosses the
8228 8445
 threshold value in the appropriate direction.
8229 8446
 
8447
+This was previously written with |previousValue| initialized negative and a
8448
+check that |previousValue| was non-negative. When the |ThresholdDetector| is
8449
+connected to a data source representing temperature measurements this is a
8450
+reasonable choice, however it breaks when connected to a rate of change series.
8451
+To make this more generally correct, a boolean is checked to determine if a
8452
+previous value has been set.
8453
+
8230 8454
 @<ThresholdDetector Implementation@>=
8231 8455
 void ThresholdDetector::newMeasurement(Measurement measure)
8232 8456
 {
8233 8457
     if((currentDirection == Ascending && previousValue < threshold &&
8234
-       previousValue >= 0) || (currentDirection == Descending &&
8235
-       previousValue > threshold && previousValue >= 0))
8458
+       previousValueValid) || (currentDirection == Descending &&
8459
+       previousValue > threshold && previousValueValid))
8236 8460
     {
8237 8461
         if((currentDirection == Ascending && measure.temperature() >= threshold) ||
8238 8462
            (currentDirection == Descending && measure.temperature() <= threshold))
@@ -8245,9 +8469,11 @@ void ThresholdDetector::newMeasurement(Measurement measure)
8245 8469
         }
8246 8470
     }
8247 8471
     previousValue = measure.temperature();
8472
+    previousValueValid = true;
8248 8473
 }
8249 8474
 
8250 8475
 ThresholdDetector::ThresholdDetector(double value) : QObject(NULL),
8476
+    previousValueValid(false),
8251 8477
     previousValue(-1), threshold(value), currentDirection(Ascending)
8252 8478
 {
8253 8479
     /* Nothing needs to be done here. */
@@ -8956,6 +9182,9 @@ space, but it is very fast and simple to code.
8956 9182
 Starting in version 1.4, column sizes are persisted automatically using the
8957 9183
 same method as described in the section on |SqlQueryView|.
8958 9184
 
9185
+Starting in version 1.8, |rowCount()| is |Q_INVOKABLE|. This allows the manual
9186
+log entry interface to easily determine if any roasting data exists to save.
9187
+
8959 9188
 @<Class declarations@>=
8960 9189
 class MeasurementModel;@/
8961 9190
 class ZoomLog : public QTableView@/
@@ -8970,7 +9199,7 @@ class ZoomLog : public QTableView@/
8970 9199
     public:@/
8971 9200
         ZoomLog();
8972 9201
         QVariant data(int row, int column) const;
8973
-        int rowCount();
9202
+        @[Q_INVOKABLE@,@, int rowCount();
8974 9203
         bool saveXML(QIODevice *device);
8975 9204
         bool saveCSV(QIODevice *device);
8976 9205
         QString lastTime(int series);
@@ -9102,11 +9331,11 @@ annotation associated with it. The solution in this case is to synthesize
9102 9331
 measurements so that the |ZoomLog| thinks it gets at least one measurement
9103 9332
 every second.
9104 9333
 
9105
-The current approach simply replicates the last measurement every second until
9106
-the time for the most recent measurement is reached, however it would likely be
9107
-better to interpolate values between the two most recent real measurements as
9108
-this would match the graphic representation rather than altering it when later
9109
-reviewing the batch.
9334
+Prior to version 1.8 this simply replicated the last measurement every second
9335
+until the time for the most recent measurement was reached, however this yields
9336
+problematic results when loading saved data or attempting to use this view for
9337
+manual data entry. The current behavior performs a linear interpolation which
9338
+will match the graph.
9110 9339
 
9111 9340
 @<Synthesize measurements for slow hardware@>=
9112 9341
 if(lastMeasurement.contains(tempcolumn))
@@ -9114,14 +9343,23 @@ if(lastMeasurement.contains(tempcolumn))
9114 9343
     if(lastMeasurement[tempcolumn].time() < measure.time())
9115 9344
     {
9116 9345
         QList<QTime> timelist;
9346
+        QList<double> templist;
9347
+        QTime z = QTime(0, 0, 0, 0);
9348
+        double ptime = (double)(z.secsTo(lastMeasurement[tempcolumn].time()));
9349
+        double ptemp = lastMeasurement[tempcolumn].temperature();
9350
+        double ctime = (double)(z.secsTo(measure.time()));
9351
+        double ctemp = measure.temperature();
9117 9352
         for(QTime i = lastMeasurement.value(tempcolumn).time().addSecs(1); i < measure.time(); i = i.addSecs(1))
9118 9353
         {
9119 9354
             timelist.append(i);
9355
+            double v = ((ptemp * (ctime - z.secsTo(i))) + (ctemp * (z.secsTo(i) - ptime))) / (ctime - ptime);
9356
+            templist.append(v);
9120 9357
         }
9121 9358
         for(int i = 0; i < timelist.size(); i++)
9122 9359
         {
9123 9360
             Measurement synthesized = measure;
9124 9361
             synthesized.setTime(timelist[i]);
9362
+            synthesized.setTemperature(templist[i]);
9125 9363
             newMeasurement(synthesized, tempcolumn);
9126 9364
         }
9127 9365
     }
@@ -12569,6 +12807,8 @@ void CSVOutput::setDevice(QIODevice *device)
12569 12807
 
12570 12808
 @i webview.w
12571 12809
 
12810
+@i printerselector.w
12811
+
12572 12812
 @* The Application class.
12573 12813
 
12574 12814
 The |Application| class represents the \pn{} program. It is responsible for
@@ -12589,19 +12829,30 @@ class Application : public QApplication@/
12589 12829
         QDomDocument* configuration();
12590 12830
         @<Device configuration members@>@;
12591 12831
         QSqlDatabase database();
12832
+        Q_INVOKABLE bool databaseConnected();
12833
+        Q_INVOKABLE QString currentTypicaUser();
12834
+        Q_INVOKABLE bool login(const QString &user, const QString &password);
12835
+        Q_INVOKABLE bool autoLogin();
12592 12836
         QScriptEngine *engine;@/
12593 12837
     @[public slots@]:@/
12838
+	    void setDatabaseConnected(bool status);
12839
+	    void setCurrentTypicaUser(const QString &user);
12594 12840
         @<Extended Application slots@>@;
12841
+    @[signals@]:@/
12842
+	    void userChanged(const QString &user);
12595 12843
     private:@/
12596 12844
         @<Application private data members@>@;
12597 12845
         QDomDocument conf;
12846
+        bool connectionStatus;
12847
+        QString currentUser;
12598 12848
 };
12599 12849
 
12600 12850
 @ The constructor for this class handles a few things that had previously been
12601 12851
 handled in |main()|.
12602 12852
 
12603 12853
 @<Application Implementation@>=
12604
-Application::Application(int &argc, char **argv) : QApplication(argc, argv)@/
12854
+Application::Application(int &argc, char **argv) : QApplication(argc, argv),
12855
+	connectionStatus(false), currentUser(QString())@/
12605 12856
 {
12606 12857
     @<Allow use of the default QSettings constructor@>@;
12607 12858
     @<Load translation objects@>@;
@@ -12669,6 +12920,20 @@ QSqlDatabase Application::database()
12669 12920
     return QSqlDatabase::cloneDatabase(connection, QString(connectionName));
12670 12921
 }
12671 12922
 
12923
+@ Starting with version 1.8 there are methods for determining if a connection
12924
+to the database was successfully established when Typica was opened.
12925
+
12926
+@<Application Implementation@>=
12927
+void Application::setDatabaseConnected(bool status)
12928
+{
12929
+	connectionStatus = status;
12930
+}
12931
+
12932
+bool Application::databaseConnected()
12933
+{
12934
+	return connectionStatus;
12935
+}
12936
+
12672 12937
 @** Table editor for ordered arrays with SQL relations.
12673 12938
 
12674 12939
 \noindent A database in use at Wilson's Coffee \char'046~Tea stores information
@@ -13495,15 +13760,20 @@ SqlConnectionSetup::SqlConnectionSetup() :
13495 13760
     cancelButton(new QPushButton(tr("Cancel"))),
13496 13761
     connectButton(new QPushButton(tr("Connect")))@/
13497 13762
 {
13763
+	QSettings settings;
13498 13764
     driver->addItem("PostgreSQL", "QPSQL");
13499 13765
     formLayout->addRow(tr("Database driver:"), driver);
13500 13766
     formLayout->addRow(tr("Host name:"), hostname);
13767
+    hostname->setText(settings.value("database/hostname").toString());
13501 13768
     formLayout->addRow(tr("Port number:"), portnumber);
13502
-    portnumber->setText("5432");
13769
+    portnumber->setText(settings.value("database/portnumber", "5432").toString());
13503 13770
     formLayout->addRow(tr("Database name:"), dbname);
13771
+    dbname->setText(settings.value("database/dbname").toString());
13504 13772
     formLayout->addRow(tr("User name:"), user);
13773
+    user->setText(settings.value("database/user").toString());
13505 13774
     password->setEchoMode(QLineEdit::Password);
13506 13775
     formLayout->addRow(tr("Password:"), password);
13776
+    password->setText(settings.value("database/password").toString());
13507 13777
     layout->addLayout(formLayout);
13508 13778
     buttons->addStretch(1);
13509 13779
     buttons->addWidget(cancelButton);
@@ -13548,6 +13818,7 @@ void SqlConnectionSetup::testConnection()
13548 13818
         settings.setValue("database/user", user->text());
13549 13819
         settings.setValue("database/password", password->text());
13550 13820
         database.close();
13821
+        AppInstance->setDatabaseConnected(true);
13551 13822
         accept();
13552 13823
     }
13553 13824
     else
@@ -13587,6 +13858,7 @@ if(!database.open())
13587 13858
 else
13588 13859
 {
13589 13860
     database.close();
13861
+    AppInstance->setDatabaseConnected(true);
13590 13862
 }
13591 13863
 
13592 13864
 @** Viewing a record of batches.
@@ -14293,6 +14565,8 @@ void setQTextEditProperties(QScriptValue value, QScriptEngine *engine)
14293 14565
     value.setProperty("print", engine->newFunction(QTextEdit_print));
14294 14566
 }
14295 14567
 
14568
+@i plugins.w
14569
+
14296 14570
 @i daterangeselector.w
14297 14571
 
14298 14572
 @** An area for repeated user interface elements.
@@ -15325,7 +15599,7 @@ class DeviceTreeModel : public QAbstractItemModel@/
15325 15599
         QModelIndex index(int row, int column,
15326 15600
                           const QModelIndex &parent = QModelIndex()) const;
15327 15601
         QModelIndex parent(const QModelIndex &child) const;
15328
-        int rowCount(const QModelIndex &parent = QModelIndex()) const;
15602
+        Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const;
15329 15603
         int columnCount(const QModelIndex &parent = QModelIndex()) const;
15330 15604
         bool setData(const QModelIndex &index, const QVariant &value,
15331 15605
                      int role);
@@ -17098,7 +17372,11 @@ StopSelector::StopSelector(QWidget *parent) : QComboBox(parent)
17098 17372
 
17099 17373
 @** Configuration of Serial Devices Using Modbus RTU.
17100 17374
 
17101
-\noindent One protocol that is used across a broad class of devices is called
17375
+\noindent The following sections contain the details of older code related to
17376
+Modbus RTU support. While the various selector widgets are shared with the new
17377
+code, the configuration widgets and device interfaces are being replaced.
17378
+
17379
+One protocol that is used across a broad class of devices is called
17102 17380
 Modbus RTU. This protocol allows multiple devices to be chained together on a
17103 17381
 two wire bus which can be connected to a single serial port. The communication
17104 17382
 protocol involves a single message which is sent from a master device (in this
@@ -18171,7 +18449,6 @@ ModbusRTUDevice::ModbusRTUDevice(DeviceTreeModel *model,@| const QModelIndex &in
18171 18449
 : QObject(NULL), messageDelayTimer(new QTimer), commTimeout(new QTimer), unitIsF(@[true@]), readingsv(@[false@]),
18172 18450
     waiting(@[false@])@/
18173 18451
 {@/
18174
-qDebug() << "Initializing Modbus RTU Device";
18175 18452
     QDomElement portReferenceElement = model->referenceElement(model->data(index,
18176 18453
         Qt::UserRole).toString());
18177 18454
     QDomNodeList portConfigData = portReferenceElement.elementsByTagName("attribute");
@@ -18657,6 +18934,7 @@ void ModbusRTUDevice::sendNextMessage()
18657 18934
         char *check = (char*)&crc;
18658 18935
         message.append(check[0]);
18659 18936
         message.append(check[1]);
18937
+        qDebug() << "Writing" << message.toHex();
18660 18938
         port->write(message);
18661 18939
         commTimeout->start(2000);
18662 18940
         messageDelayTimer->start(delayTime);
@@ -19330,6 +19608,8 @@ app.registerDeviceConfigurationWidget("modbusrtu", ModbusConfigurator::staticMet
19330 19608
 inserter = new NodeInserter(tr("Modbus RTU Device"), tr("Modbus RTU Device"), "modbusrtu", NULL);
19331 19609
 topLevelNodeInserters.append(inserter);
19332 19610
 
19611
+@i modbus.w
19612
+
19333 19613
 @i unsupportedserial.w
19334 19614
 
19335 19615
 @i phidgets.w
@@ -19898,9 +20178,11 @@ class TranslationConfWidget : public BasicDeviceConfigurationWidget
19898 20178
     @[private slots@]:@/
19899 20179
         void updateMatchingColumn(const QString &column);
19900 20180
         void updateTemperature();
20181
+        void updateDelay();
19901 20182
     private:@/
19902 20183
         QDoubleSpinBox *temperatureValue;
19903 20184
         QComboBox *unitSelector;
20185
+        QSpinBox *delaySelector;
19904 20186
 };
19905 20187
 
19906 20188
 @ The constructor sets up our user interface.
@@ -19908,7 +20190,8 @@ class TranslationConfWidget : public BasicDeviceConfigurationWidget
19908 20190
 @<TranslationConfWidget implementation@>=
19909 20191
 TranslationConfWidget::TranslationConfWidget(DeviceTreeModel *model, const QModelIndex &index)
19910 20192
 : BasicDeviceConfigurationWidget(model, index),
19911
-    temperatureValue(new QDoubleSpinBox), unitSelector(new QComboBox)
20193
+    temperatureValue(new QDoubleSpinBox), unitSelector(new QComboBox),
20194
+    delaySelector(new QSpinBox)
19912 20195
 {
19913 20196
     unitSelector->addItem("Fahrenheit");
19914 20197
     unitSelector->addItem("Celsius");
@@ -19919,6 +20202,7 @@ TranslationConfWidget::TranslationConfWidget(DeviceTreeModel *model, const QMode
19919 20202
     layout->addRow(tr("Column to match:"), column);
19920 20203
     layout->addRow(tr("Unit:"), unitSelector);
19921 20204
     layout->addRow(tr("Value:"), temperatureValue);
20205
+    layout->addRow(tr("Start of batch safety delay:"), delaySelector);
19922 20206
     @<Get device configuration data for current node@>@;
19923 20207
     for(int i = 0; i < configData.size(); i++)
19924 20208
     {
@@ -19935,12 +20219,18 @@ TranslationConfWidget::TranslationConfWidget(DeviceTreeModel *model, const QMode
19935 20219
         {
19936 20220
             temperatureValue->setValue(node.attribute("value").toDouble());
19937 20221
         }
20222
+        else if(node.attribute("name") == "delay")
20223
+        {
20224
+	        delaySelector->setValue(node.attribute("value").toInt());
20225
+        }
19938 20226
     }
19939 20227
     updateMatchingColumn(column->text());
19940 20228
     updateTemperature();
20229
+    updateDelay();
19941 20230
     connect(column, SIGNAL(textEdited(QString)), this, SLOT(updateMatchingColumn(QString)));
19942 20231
     connect(unitSelector, SIGNAL(currentIndexChanged(QString)), this, SLOT(updateTemperature()));
19943 20232
     connect(temperatureValue, SIGNAL(valueChanged(double)), this, SLOT(updateTemperature()));
20233
+    connect(delaySelector, SIGNAL(valueChanged(int)), this, SLOT(updateDelay()));
19944 20234
     setLayout(layout);
19945 20235
 }
19946 20236
 
@@ -19968,6 +20258,11 @@ void TranslationConfWidget::updateMatchingColumn(const QString &column)
19968 20258
     updateAttribute("column", column);
19969 20259
 }
19970 20260
 
20261
+void TranslationConfWidget::updateDelay()
20262
+{
20263
+	updateAttribute("delay", QString("%1").arg(delaySelector->value()));
20264
+}
20265
+
19971 20266
 @ This is registered with the configuration system.
19972 20267
 
19973 20268
 @<Register device configuration widgets@>=
@@ -19975,12 +20270,18 @@ app.registerDeviceConfigurationWidget("translation", TranslationConfWidget::stat
19975 20270
 
19976 20271
 @i rate.w
19977 20272
 
20273
+@i mergeseries.w
20274
+
19978 20275
 @i dataqsdk.w
19979 20276
 
19980 20277
 @i scales.w
19981 20278
 
19982 20279
 @i valueannotation.w
19983 20280
 
20281
+@i thresholdannotation.w
20282
+
20283
+@i user.w
20284
+
19984 20285
 @** Local changes.
19985 20286
 
19986 20287
 \noindent This is the end of \pn{} as distributed by its author. It is expected

+ 6
- 6
src/units.cpp View File

@@ -1,10 +1,10 @@
1
-/*279:*/
1
+/*291:*/
2 2
 #line 42 "./units.w"
3 3
 
4 4
 #include "units.h"
5 5
 #include <QtDebug> 
6 6
 
7
-/*:279*//*280:*/
7
+/*:291*//*292:*/
8 8
 #line 53 "./units.w"
9 9
 
10 10
 bool Units::isTemperatureUnit(Unit unit)
@@ -15,7 +15,7 @@ unit==Kelvin||
15 15
 unit==Rankine);
16 16
 }
17 17
 
18
-/*:280*//*281:*/
18
+/*:292*//*293:*/
19 19
 #line 71 "./units.w"
20 20
 
21 21
 double Units::convertTemperature(double value,Unit fromUnit,Unit toUnit)
@@ -113,7 +113,7 @@ break;
113 113
 return 0;
114 114
 }
115 115
 
116
-/*:281*//*282:*/
116
+/*:293*//*294:*/
117 117
 #line 169 "./units.w"
118 118
 
119 119
 double Units::convertRelativeTemperature(double value,Unit fromUnit,Unit toUnit)
@@ -211,7 +211,7 @@ break;
211 211
 return 0;
212 212
 }
213 213
 
214
-/*:282*//*283:*/
214
+/*:294*//*295:*/
215 215
 #line 267 "./units.w"
216 216
 
217 217
 double Units::convertWeight(double value,Unit fromUnit,Unit toUnit)
@@ -316,4 +316,4 @@ unit==Ounce||
316 316
 unit==Gram);
317 317
 }
318 318
 
319
-/*:283*/
319
+/*:295*/

+ 2
- 2
src/units.h View File

@@ -1,4 +1,4 @@
1
-/*278:*/
1
+/*290:*/
2 2
 #line 8 "./units.w"
3 3
 
4 4
 #include <QObject> 
@@ -32,4 +32,4 @@ static bool isWeightUnit(Unit unit);
32 32
 
33 33
 #endif
34 34
 
35
-/*:278*/
35
+/*:290*/

+ 0
- 38
src/unsupportedserial.w View File

@@ -692,44 +692,6 @@ QScriptValue SerialPort_flush(QScriptContext *context, QScriptEngine *)
692 692
 	return QScriptValue();
693 693
 }
694 694
 
695
-@* Timers.
696
-
697
-\noindent While some devices will output a steady stream of measurements which
698
-can be continuously read as they come in, other devices must be polled for
699
-their current state. One approach is to poll the device immediately after
700
-reading the response from the previous polling, but there are times when we may
701
-want to limit the rate at which we poll the device. There are also devices
702
-which specify a length of time during which data should not be sent. For these
703
-cases, we expose |QTimer| to the host environment which allows us to wait. This
704
-is also useful for producing simulations to test features without needing to be
705
-connected to real hardware.
706
-
707
-<@Function prototypes for scripting@>=
708
-void setQTimerProperties(QScriptValue value, QScriptEngine *engine);
709
-QScriptValue constructQTimer(QScriptContext *context, QScriptEngine *engine);
710
-
711
-@ The host environment is informed of the constructor.
712
-
713
-@<Set up the scripting engine@>=
714
-constructor = engine->newFunction(constructQTimer);
715
-value = engine->newQMetaObject(&QTimer::staticMetaObject, constructor);
716
-engine->globalObject().setProperty("Timer", value);
717
-
718
-@ Everything that we are interested in here is a signal, slot, or property so
719
-there is little else to do.
720
-
721
-@<Functions for scripting@>=
722
-void setQTimerProperties(QScriptValue value, QScriptEngine *engine)
723
-{
724
-	setQObjectProperties(value, engine);
725
-}
726
-
727
-QScriptValue constructQTimer(QScriptContext *, QScriptEngine *engine)
728
-{
729
-	QScriptValue object = engine->newQObject(new QTimer);
730
-	setQTimerProperties(object, engine);
731
-	return object;
732
-}
733 695
 
734 696
 
735 697
 

+ 347
- 0
src/user.w View File

@@ -0,0 +1,347 @@
1
+@** Typica User Management.
2
+
3
+\noindent Starting in version 1.8, the concepts of database user and \pn{} user
4
+are separated. This means that there must be controls for creating new users
5
+and for selecting the user to log in as. Other management interfaces can be
6
+implemented in configuration scripts.
7
+
8
+@* Application extensions for user handling.
9
+
10
+\noindent In order to present information about the currently logged in user
11
+globally, it was decided to provide a few methods in |Application| that can be
12
+used to report and change the current user.
13
+
14
+The first of these simply reports the currently logged in user.
15
+
16
+@<Application Implementation@>=
17
+QString Application::currentTypicaUser()
18
+{
19
+	return currentUser;
20
+}
21
+
22
+@ Next is a method that can be used to force the login of a specified user
23
+without checking for an entered password. This is used for users that are set
24
+to login automatically.
25
+
26
+@<Application Implementation@>=
27
+void Application::setCurrentTypicaUser(const QString &user)
28
+{
29
+	currentUser = user;
30
+	emit userChanged(currentUser);
31
+}
32
+
33
+@ A login method is provided which determines if a user exists that matches the
34
+user name and password specified and reports if the login attempt was
35
+successful.
36
+
37
+@<Application Implementation@>=
38
+bool Application::login(const QString &user, const QString &password)
39
+{
40
+	SqlQueryConnection h;
41
+	QSqlQuery *dbquery = h.operator->();
42
+	dbquery->prepare("SELECT 1 FROM typica_users WHERE name = :name AND password = :password AND active = TRUE");
43
+	dbquery->bindValue(":name", user);
44
+	dbquery->bindValue(":password", password);
45
+	dbquery->exec();
46
+	if(dbquery->next())
47
+	{
48
+		currentUser = user;
49
+		emit userChanged(currentUser);
50
+		return true;
51
+	}
52
+	return false;
53
+}
54
+
55
+@ A convenience method is also provided to attempt an automatic login if one is
56
+specified in the database.
57
+
58
+@<Application Implementation@>=
59
+bool Application::autoLogin()
60
+{
61
+	SqlQueryConnection h;
62
+	QSqlQuery *dbquery = h.operator->();
63
+	dbquery->exec("SELECT name FROM typica_users WHERE auto_login = TRUE");
64
+	if(dbquery->next())
65
+	{
66
+		currentUser = dbquery->value(0).toString();
67
+		emit userChanged(currentUser);
68
+		return true;
69
+	}
70
+	return false;
71
+}
72
+
73
+@* Login dialog.
74
+
75
+\noindent If there are no users set to log in automatically or any time a user
76
+change is requested, a login dialog should be presented.
77
+
78
+@<Class declarations@>=
79
+class LoginDialog : public QDialog
80
+{
81
+	Q_OBJECT
82
+	public:
83
+		LoginDialog();
84
+	public slots:
85
+		void attemptLogin();
86
+	private:
87
+		QLineEdit *user;
88
+		QLineEdit *password;
89
+		QLabel *warning;
90
+		QPushButton *login;
91
+};
92
+
93
+@ The constructor sets up the interface.
94
+
95
+@<LoginDialog implementation@>=
96
+LoginDialog::LoginDialog() : QDialog(),
97
+	user(new QLineEdit), password(new QLineEdit),
98
+	warning(new QLabel(tr("Log in failed."))),
99
+	login(new QPushButton(tr("Log In")))
100
+{
101
+	setModal(true);
102
+	QVBoxLayout *mainLayout = new QVBoxLayout;
103
+	warning->setVisible(false);
104
+	password->setEchoMode(QLineEdit::Password);
105
+	QFormLayout *form = new QFormLayout;
106
+	form->addRow(tr("Name:"), user);
107
+	form->addRow(tr("Password:"), password);
108
+	form->addRow(warning);
109
+	QHBoxLayout *buttonBox = new QHBoxLayout;
110
+	buttonBox->addStretch();
111
+	buttonBox->addWidget(login);
112
+	mainLayout->addLayout(form);
113
+	mainLayout->addLayout(buttonBox);
114
+	connect(login, SIGNAL(clicked()), this, SLOT(attemptLogin()));
115
+	setLayout(mainLayout);
116
+}
117
+
118
+@ The log in button attempts to log in with the specified credentials.
119
+
120
+@<LoginDialog implementation@>=
121
+void LoginDialog::attemptLogin()
122
+{
123
+	if(AppInstance->login(user->text(), password->text()))
124
+	{
125
+		accept();
126
+	}
127
+	else
128
+	{
129
+		warning->setVisible(true);
130
+	}
131
+}
132
+
133
+@ Scripts must be able to create login dialogs.
134
+
135
+@<Set up the scripting engine@>=
136
+constructor = engine->newFunction(constructLoginDialog);
137
+value = engine->newQMetaObject(&LoginDialog::staticMetaObject, constructor);
138
+engine->globalObject().setProperty("LoginDialog", value);
139
+
140
+@ The constructor is trivial.
141
+
142
+@<Functions for scripting@>=
143
+QScriptValue constructLoginDialog(QScriptContext *, QScriptEngine *engine)
144
+{
145
+	QScriptValue object = engine->newQObject(new LoginDialog);
146
+	return object;
147
+}
148
+
149
+@ A function prototype is required.
150
+
151
+@<Function prototypes for scripting@>=
152
+QScriptValue constructLoginDialog(QScriptContext *context, QScriptEngine *engine);
153
+
154
+@* Currently logged in user.
155
+
156
+\noindent Every main window in \pn{} should be able to report on the currently
157
+logged in user and it should be possible to bring up an interface to switch
158
+users. An easy way to do this is through a widget inserted into the status bar
159
+of every window that listens for user change data from the |Application|
160
+instance.
161
+
162
+@<Class declarations@>=
163
+class UserLabel : public QLabel
164
+{
165
+	Q_OBJECT
166
+	public:
167
+		UserLabel();
168
+	public slots:
169
+		void updateLabel(const QString &user);
170
+	protected:
171
+		void mouseReleaseEvent(QMouseEvent *event);
172
+};
173
+
174
+@ On first instantiation, the constructor sets the displayed text to indicate
175
+the currently logged in user and starts listening for user change events.
176
+
177
+@<UserLabel implementation@>=
178
+UserLabel::UserLabel() : QLabel()
179
+{
180
+	setTextFormat(Qt::PlainText);
181
+	updateLabel(AppInstance->currentTypicaUser());
182
+	connect(AppInstance, SIGNAL(userChanged(QString)),
183
+	        this, SLOT(updateLabel(QString)));
184
+}
185
+
186
+@ When the currently logged in user changes, the label text updates itself.
187
+
188
+@<UserLabel implementation@>=
189
+void UserLabel::updateLabel(const QString &user)
190
+{
191
+	setText(QString(tr("Current operator: %1").arg(user)));
192
+}
193
+
194
+@ In order to handle clicks, |mouseReleaseEvent()| is implemented.
195
+
196
+@<UserLabel implementation@>=
197
+void UserLabel::mouseReleaseEvent(QMouseEvent *event)
198
+{
199
+	LoginDialog *dialog = new LoginDialog;
200
+	dialog->exec();
201
+}
202
+
203
+@* User Creation.
204
+
205
+\noindent The first time \pn{} is started with a database connection and a
206
+multi-user aware configuration there will not be any user records in the
207
+database. An interface for adding new users is provided.
208
+
209
+@<Class declarations@>=
210
+class NewTypicaUser: public QDialog
211
+{
212
+	Q_OBJECT
213
+	public:
214
+		NewTypicaUser();
215
+	public slots:
216
+		void createAndReset();
217
+		void createAndClose();
218
+		void validate();
219
+		void cancelValidate();
220
+	private:
221
+		void createNewUser();
222
+		QLineEdit *userField;
223
+		QLineEdit *passwordField;
224
+		QCheckBox *autoLogin;
225
+		QPushButton *saveAndCloseButton;
226
+		QPushButton *saveAndNewButton;
227
+		QPushButton *cancelButton;
228
+};
229
+
230
+@ The constructor sets up the dialog.
231
+
232
+@<NewTypicaUser implementation@>=
233
+NewTypicaUser::NewTypicaUser() : QDialog(),
234
+	userField(new QLineEdit), passwordField(new QLineEdit),
235
+	autoLogin(new QCheckBox(tr("Log in automatically"))),
236
+	saveAndCloseButton(new QPushButton(tr("Save and Close"))),
237
+	saveAndNewButton(new QPushButton(tr("Save and Create Another"))),
238
+	cancelButton(new QPushButton(tr("Cancel")))
239
+{
240
+	setModal(true);
241
+	QVBoxLayout *mainLayout = new QVBoxLayout;
242
+	QFormLayout *form = new QFormLayout;
243
+	QHBoxLayout *buttons = new QHBoxLayout;
244
+	form->addRow(tr("Name:"), userField);
245
+	passwordField->setEchoMode(QLineEdit::Password);
246
+	form->addRow(tr("Password:"), passwordField);
247
+	form->addRow(autoLogin);
248
+	buttons->addWidget(cancelButton);
249
+	buttons->addStretch();
250
+	buttons->addWidget(saveAndNewButton);
251
+	buttons->addWidget(saveAndCloseButton);
252
+	mainLayout->addLayout(form);
253
+	mainLayout->addLayout(buttons);
254
+	setLayout(mainLayout);
255
+	setWindowTitle(tr("Create New User"));
256
+	connect(cancelButton, SIGNAL(clicked()), this, SLOT(reject()));
257
+	connect(saveAndCloseButton, SIGNAL(clicked()), this, SLOT(createAndClose()));
258
+	connect(saveAndNewButton, SIGNAL(clicked()), this, SLOT(createAndReset()));
259
+	connect(userField, SIGNAL(textChanged(QString)), this, SLOT(validate()));
260
+}
261
+
262
+@ Slots handle basic operation.
263
+
264
+@<NewTypicaUser implementation@>=
265
+void NewTypicaUser::createAndReset()
266
+{
267
+	createNewUser();
268
+	userField->setText("");
269
+	passwordField->setText("");
270
+	autoLogin->setChecked(false);
271
+}
272
+
273
+void NewTypicaUser::createAndClose()
274
+{
275
+	createNewUser();
276
+	accept();
277
+}
278
+
279
+void NewTypicaUser::createNewUser()
280
+{
281
+	SqlQueryConnection h;
282
+	QSqlQuery *dbquery = h.operator->();
283
+	dbquery->prepare("INSERT INTO typica_users (name, password, active, auto_login) VALUES (:name, :password, true, :auto)");
284
+	dbquery->bindValue(":name", userField->text());
285
+	dbquery->bindValue(":password", passwordField->text());
286
+	dbquery->bindValue(":auto", autoLogin->isChecked());
287
+	dbquery->exec();
288
+	cancelButton->setEnabled(true);
289
+}
290
+
291
+void NewTypicaUser::validate()
292
+{
293
+	if(!userField->text().isEmpty())
294
+	{
295
+		saveAndCloseButton->setEnabled(true);
296
+		saveAndNewButton->setEnabled(true);
297
+	}
298
+	else
299
+	{
300
+		saveAndCloseButton->setEnabled(false);
301
+		saveAndNewButton->setEnabled(false);
302
+	}
303
+}
304
+
305
+void NewTypicaUser::cancelValidate()
306
+{
307
+	SqlQueryConnection h;
308
+	QSqlQuery *dbquery = h.operator->();
309
+	dbquery->exec("SELECT count(1) FROM typica_users");
310
+	if(dbquery->next())
311
+	{
312
+		if(dbquery->value(0).toInt() > 0)
313
+		{
314
+			cancelButton->setEnabled(true);
315
+			return;
316
+		}
317
+	}
318
+	cancelButton->setEnabled(false);
319
+}
320
+
321
+@ This is exposted to the host environment in the usual way.
322
+
323
+@<Set up the scripting engine@>=
324
+constructor = engine->newFunction(constructNewTypicaUser);
325
+value = engine->newQMetaObject(&NewTypicaUser::staticMetaObject, constructor);
326
+engine->globalObject().setProperty("NewTypicaUser", value);
327
+
328
+@ The constructor is trivial.
329
+
330
+@<Functions for scripting@>=
331
+QScriptValue constructNewTypicaUser(QScriptContext *, QScriptEngine *engine)
332
+{
333
+	QScriptValue object = engine->newQObject(new NewTypicaUser);
334
+	return object;
335
+}
336
+
337
+@ A function prototype is required.
338
+
339
+@<Function prototypes for scripting@>=
340
+QScriptValue constructNewTypicaUser(QScriptContext *context, QScriptEngine *engine);
341
+
342
+@ Add class implementations to generated source file.
343
+
344
+@<Class implementations@>=
345
+@<NewTypicaUser implementation@>
346
+@<UserLabel implementation@>
347
+@<LoginDialog implementation@>

+ 9
- 9
src/webelement.cpp View File

@@ -1,18 +1,18 @@
1
-/*573:*/
2
-#line 368 "./webview.w"
1
+/*586:*/
2
+#line 383 "./webview.w"
3 3
 
4 4
 #include "webelement.h"
5 5
 
6
-/*571:*/
7
-#line 311 "./webview.w"
6
+/*584:*/
7
+#line 326 "./webview.w"
8 8
 
9 9
 TypicaWebElement::TypicaWebElement(QWebElement element):e(element)
10 10
 {
11 11
 
12 12
 }
13 13
 
14
-/*:571*//*572:*/
15
-#line 320 "./webview.w"
14
+/*:584*//*585:*/
15
+#line 335 "./webview.w"
16 16
 
17 17
 void TypicaWebElement::appendInside(const QString&markup)
18 18
 {
@@ -59,8 +59,8 @@ void TypicaWebElement::setPlainText(const QString&text)
59 59
 e.setPlainText(text);
60 60
 }
61 61
 
62
-/*:572*/
63
-#line 371 "./webview.w"
62
+/*:585*/
63
+#line 386 "./webview.w"
64 64
 
65 65
 
66
-/*:573*/
66
+/*:586*/

+ 3
- 3
src/webelement.h View File

@@ -1,5 +1,5 @@
1
-/*566:*/
2
-#line 248 "./webview.w"
1
+/*579:*/
2
+#line 263 "./webview.w"
3 3
 
4 4
 #include <QWebElement> 
5 5
 #include <QObject> 
@@ -27,4 +27,4 @@ QWebElement e;
27 27
 
28 28
 #endif
29 29
 
30
-/*:566*/
30
+/*:579*/

+ 32
- 22
src/webview.cpp View File

@@ -1,9 +1,9 @@
1
-/*551:*/
1
+/*563:*/
2 2
 #line 50 "./webview.w"
3 3
 
4 4
 #include "webview.h"
5 5
 
6
-/*552:*/
6
+/*564:*/
7 7
 #line 57 "./webview.w"
8 8
 
9 9
 TypicaWebView::TypicaWebView():QWebView()
@@ -12,7 +12,7 @@ page()->setLinkDelegationPolicy(QWebPage::DelegateExternalLinks);
12 12
 connect(page(),SIGNAL(linkClicked(QUrl)),this,SLOT(linkDelegate(QUrl)));
13 13
 }
14 14
 
15
-/*:552*//*553:*/
15
+/*:564*//*565:*/
16 16
 #line 73 "./webview.w"
17 17
 
18 18
 void TypicaWebView::linkDelegate(const QUrl&url)
@@ -20,7 +20,7 @@ void TypicaWebView::linkDelegate(const QUrl&url)
20 20
 if(url.scheme()=="typica")
21 21
 {
22 22
 QString address(url.toEncoded());
23
-/*554:*/
23
+/*566:*/
24 24
 #line 91 "./webview.w"
25 25
 
26 26
 if(address=="typica://aboutqt")
@@ -29,10 +29,10 @@ QMessageBox::aboutQt(this);
29 29
 return;
30 30
 }
31 31
 
32
-/*:554*/
32
+/*:566*/
33 33
 #line 79 "./webview.w"
34 34
 
35
-/*555:*/
35
+/*567:*/
36 36
 #line 100 "./webview.w"
37 37
 
38 38
 if(address.startsWith("typica://script/"))
@@ -41,7 +41,7 @@ emit scriptLinkClicked(address.remove(0,16));
41 41
 return;
42 42
 }
43 43
 
44
-/*:555*/
44
+/*:567*/
45 45
 #line 80 "./webview.w"
46 46
 
47 47
 }
@@ -51,7 +51,7 @@ QDesktopServices::openUrl(url);
51 51
 }
52 52
 }
53 53
 
54
-/*:553*//*556:*/
54
+/*:565*//*568:*/
55 55
 #line 112 "./webview.w"
56 56
 
57 57
 void TypicaWebView::load(const QString&url)
@@ -59,16 +59,6 @@ void TypicaWebView::load(const QString&url)
59 59
 QWebView::load(QUrl(url));
60 60
 }
61 61
 
62
-void TypicaWebView::print()
63
-{
64
-QPrinter*printer= new QPrinter(QPrinter::HighResolution);
65
-QPrintDialog printDialog(printer,NULL);
66
-if(printDialog.exec()==QDialog::Accepted)
67
-{
68
-QWebView::print(printer);
69
-}
70
-}
71
-
72 62
 void TypicaWebView::setHtml(const QString&html,const QUrl&baseUrl)
73 63
 {
74 64
 QWebView::setHtml(html,baseUrl);
@@ -88,8 +78,28 @@ QString TypicaWebView::saveXml()
88 78
 return page()->currentFrame()->documentElement().toOuterXml();
89 79
 }
90 80
 
91
-/*:556*//*562:*/
92
-#line 205 "./webview.w"
81
+/*:568*//*569:*/
82
+#line 144 "./webview.w"
83
+
84
+void TypicaWebView::print(const QString&printerName)
85
+{
86
+QPrinter*printer= new QPrinter(QPrinter::HighResolution);
87
+if(!printerName.isEmpty())
88
+{
89
+printer->setPrinterName(printerName);
90
+printer->setFullPage(true);
91
+QWebView::print(printer);
92
+return;
93
+}
94
+QPrintDialog printDialog(printer,NULL);
95
+if(printDialog.exec()==QDialog::Accepted)
96
+{
97
+QWebView::print(printer);
98
+}
99
+}
100
+
101
+/*:569*//*575:*/
102
+#line 220 "./webview.w"
93 103
 
94 104
 QWebElement TypicaWebView::documentElement()
95 105
 {
@@ -101,8 +111,8 @@ QWebElement TypicaWebView::findFirstElement(const QString&selector)
101 111
 return page()->mainFrame()->findFirstElement(selector);
102 112
 }
103 113
 
104
-/*:562*/
114
+/*:575*/
105 115
 #line 53 "./webview.w"
106 116
 
107 117
 
108
-/*:551*/
118
+/*:563*/

+ 3
- 3
src/webview.h View File

@@ -1,4 +1,4 @@
1
-/*550:*/
1
+/*562:*/
2 2
 #line 14 "./webview.w"
3 3
 
4 4
 #include <QWebView> 
@@ -20,7 +20,7 @@ Q_OBJECT
20 20
 public:
21 21
 TypicaWebView();
22 22
 Q_INVOKABLE void load(const QString&url);
23
-Q_INVOKABLE void print();
23
+Q_INVOKABLE void print(const QString&printerName= QString());
24 24
 Q_INVOKABLE void setHtml(const QString&html,const QUrl&baseUrl= QUrl());
25 25
 Q_INVOKABLE void setContent(QIODevice*device);
26 26
 Q_INVOKABLE QString saveXml();
@@ -34,4 +34,4 @@ void linkDelegate(const QUrl&url);
34 34
 
35 35
 #endif
36 36
 
37
-/*:550*/
37
+/*:562*/

+ 26
- 11
src/webview.w View File

@@ -31,7 +31,7 @@ class TypicaWebView : public QWebView@/
31 31
 	public:@/
32 32
 		TypicaWebView();
33 33
 		@[Q_INVOKABLE@,@, void@]@, load(const QString &url);@t\2\2@>@/
34
-		@[Q_INVOKABLE@,@, void@]@, print();@t\2\2@>@/
34
+		@[Q_INVOKABLE@,@, void@]@, print(const QString &printerName = QString());@t\2\2@>@/
35 35
 		@[Q_INVOKABLE@,@, void@]@, setHtml(const QString &html, const QUrl &baseUrl = QUrl());@t\2\2@>@/
36 36
 		@[Q_INVOKABLE@,@, void@]@, setContent(QIODevice *device);@t\2\2@>@/
37 37
 		@[Q_INVOKABLE@,@, QString@]@, saveXml();@t\2\2@>@/
@@ -115,16 +115,6 @@ void TypicaWebView::load(const QString &url)
115 115
 	QWebView::load(QUrl(url));
116 116
 }
117 117
 
118
-void TypicaWebView::print()
119
-{
120
-	QPrinter *printer = new QPrinter(QPrinter::HighResolution);
121
-	QPrintDialog printDialog(printer, NULL);
122
-	if(printDialog.exec() == QDialog::Accepted)
123
-	{
124
-		QWebView::print(printer);
125
-	}
126
-}
127
-
128 118
 void TypicaWebView::setHtml(const QString &html, const QUrl &baseUrl)
129 119
 {
130 120
 	QWebView::setHtml(html, baseUrl);
@@ -144,6 +134,31 @@ QString TypicaWebView::saveXml()
144 134
 	return page()->currentFrame()->documentElement().toOuterXml();
145 135
 }
146 136
 
137
+@ Print functionality has been extended to allow an optional argument. If the
138
+name of a printer is passed in, the print dialog will be bypassed. Note that
139
+when bypassing the print dialog, page margins are set to 0. This is intentional
140
+as the use case is for printing to more specialized printers where default page
141
+margins are not appropriate and CSS can be customized to ensure that all
142
+information fits in the printable area with the assumption of no margin.
143
+
144
+@<TypicaWebView implementation@>=
145
+void TypicaWebView::print(const QString &printerName)
146
+{
147
+	QPrinter *printer = new QPrinter(QPrinter::HighResolution);
148
+	if(!printerName.isEmpty())
149
+	{
150
+		printer->setPrinterName(printerName);
151
+		printer->setFullPage(true);
152
+		QWebView::print(printer);
153
+		return;
154
+	}
155
+	QPrintDialog printDialog(printer, NULL);
156
+	if(printDialog.exec() == QDialog::Accepted)
157
+	{
158
+		QWebView::print(printer);
159
+	}
160
+}
161
+
147 162
 @ Web views are exposed to the host environment in the usual manner.
148 163
 
149 164
 @<Set up the scripting engine@>=

+ 3
- 3
typica.desktop View File

@@ -1,8 +1,8 @@
1 1
 [Desktop Entry]
2
-Version=1.6.3
2
+Version=1.8.0
3 3
 Name=Typica
4
-GenericName=Coffee Roasting
5
-Comment=Data acquisition, reporting, and management for coffee roasters
4
+GenericName=Typica
5
+Comment=Coffee Roasting Operations
6 6
 Exec=typica
7 7
 Icon=typica
8 8
 Terminal=false

BIN
typica.png View File


Loading…
Cancel
Save