Browse Source

Merge branch 'release-1.8'

Neal Wilson 6 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
-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
 Typica is free software released under the MIT license as follows:
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
 Permission is hereby granteed, free of charge, to any person obtaining a copy
8
 Permission is hereby granteed, free of charge, to any person obtaining a copy
12
 of this software and associated documentation files (the "Software"), to deal
9
 of this software and associated documentation files (the "Software"), to deal
54
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
51
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
55
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
52
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
56
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
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

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

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

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
 					output.writeEndElement();
307
 					output.writeEndElement();
308
 					output.writeEndElement();	
308
 					output.writeEndElement();	
309
 					output.writeEndElement();
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
 					output.writeEndElement();
323
 					output.writeEndElement();
324
 				}
324
 				}
325
 				output.writeEndElement();
325
 				output.writeEndElement();

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

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
 					unitText = TTR("invchange", "Kg");
66
 					unitText = TTR("invchange", "Kg");
67
 				}
67
 				}
68
 				var query = new QSqlQuery();
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
 				query.prepare(q);
70
 				query.prepare(q);
71
 				query.bind(":sd1", startDate);
71
 				query.bind(":sd1", startDate);
72
 				query.bind(":sd2", startDate);
72
 				query.bind(":sd2", startDate);
74
 				query.bind(":sd4", startDate);
74
 				query.bind(":sd4", startDate);
75
 				query.bind(":sd6", startDate);
75
 				query.bind(":sd6", startDate);
76
 				query.bind(":sd7", startDate);
76
 				query.bind(":sd7", startDate);
77
+                                query.bind(":sd8", startDate);
78
+                                query.bind(":sd9", startDate);
77
 				query.bind(":ed1", endDate);
79
 				query.bind(":ed1", endDate);
78
 				query.bind(":ed2", endDate);
80
 				query.bind(":ed2", endDate);
79
 				query.bind(":ed3", endDate);
81
 				query.bind(":ed3", endDate);
80
 				query.bind(":ed4", endDate);
82
 				query.bind(":ed4", endDate);
81
 				query.bind(":ed5", endDate);
83
 				query.bind(":ed5", endDate);
82
-				query.bind(":ed6", endDate);
83
 				query.bind(":ed7", endDate);
84
 				query.bind(":ed7", endDate);
85
+                                query.bind(":ed8", endDate);
84
 				query.bind(":c1", conversion);
86
 				query.bind(":c1", conversion);
85
 				query.bind(":c2", conversion);
87
 				query.bind(":c2", conversion);
86
 				query.bind(":c3", conversion);
88
 				query.bind(":c3", conversion);
93
 				query.bind(":c11", conversion);
95
 				query.bind(":c11", conversion);
94
 				query.bind(":c12", conversion);
96
 				query.bind(":c12", conversion);
95
 				query.bind(":c13", conversion);
97
 				query.bind(":c13", conversion);
98
+                                query.bind(":c14", conversion);
99
+                                query.bind(":c15", conversion);
96
 				query.exec();
100
 				query.exec();
97
 				output.writeStartElement("table");
101
 				output.writeStartElement("table");
98
 				output.writeAttribute("rules", "groups");
102
 				output.writeAttribute("rules", "groups");
103
 				output.writeTextElement("th", TTR("invchange", "Coffee")); // 1
107
 				output.writeTextElement("th", TTR("invchange", "Coffee")); // 1
104
 				output.writeTextElement("th", TTR("invchange", "Reference")); // 2
108
 				output.writeTextElement("th", TTR("invchange", "Reference")); // 2
105
 				output.writeTextElement("th", TTR("invchange", "Starting (") + unitText + ")"); // 3
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
 				output.writeTextElement("th", TTR("invchange", "Cost")); // 11
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
 				output.writeTextElement("th", TTR("invchange", "Cost")); // 12
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
 				output.writeTextElement("th", TTR("invchange", "Cost")); // 13
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
 				output.writeTextElement("th", TTR("invchange", "Cost")); // 14
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
 				output.writeEndElement();
123
 				output.writeEndElement();
118
 				output.writeEndElement();
124
 				output.writeEndElement();
119
 				output.writeStartElement("tbody");
125
 				output.writeStartElement("tbody");
128
 				var sum9 = 0;
134
 				var sum9 = 0;
129
 				var sum15 = 0;
135
 				var sum15 = 0;
130
 				var sum7 = 0;
136
 				var sum7 = 0;
131
-				var sum14 = 0;;
137
+				var sum14 = 0;
138
+                                var loss_sum = 0;
139
+                                var loss_cost_sum = 0;
132
 				while(query.next())
140
 				while(query.next())
133
 				{
141
 				{
134
 					output.writeStartElement("tr");
142
 					output.writeStartElement("tr");
141
 					output.writeTextElement("td", query.value(1)); //Coffee
149
 					output.writeTextElement("td", query.value(1)); //Coffee
142
 					output.writeTextElement("td", query.value(2)); //Reference
150
 					output.writeTextElement("td", query.value(2)); //Reference
143
 					output.writeStartElement("td"); //Starting Wt
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
 					output.writeCDATA(parseFloat(query.value(3)).toFixed(2));
153
 					output.writeCDATA(parseFloat(query.value(3)).toFixed(2));
146
 					output.writeEndElement(); //End of Starting Wt.
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
 					output.writeStartElement("td"); //Purchase Wt
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
 					output.writeCDATA(parseFloat(query.value(4)).toFixed(2));
158
 					output.writeCDATA(parseFloat(query.value(4)).toFixed(2));
151
 					output.writeEndElement(); //End of Purchase Wt
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
 					output.writeStartElement("td"); //Use Wt
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
 					output.writeCDATA(parseFloat(query.value(5)).toFixed(2));
163
 					output.writeCDATA(parseFloat(query.value(5)).toFixed(2));
156
 					output.writeEndElement(); //End of Use Wt
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
 					output.writeStartElement("td"); //Sale Wt
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
 					output.writeCDATA(parseFloat(query.value(6)).toFixed(2));
168
 					output.writeCDATA(parseFloat(query.value(6)).toFixed(2));
161
 					output.writeEndElement(); //End of Sale Wt
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
 					output.writeEndElement(); //Adjustment Wt
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
 					output.writeStartElement("td"); //Ending Wt
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
 					output.writeEndElement(); //End of Ending Wt
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
 					output.writeEndElement();
186
 					output.writeEndElement();
174
 					sum3 += parseFloat(query.value(3));
187
 					sum3 += parseFloat(query.value(3));
175
-					sum10 += parseFloat(query.value(10));
188
+					sum10 += parseFloat(query.value(11));
176
 					sum4 += parseFloat(query.value(4));
189
 					sum4 += parseFloat(query.value(4));
177
-					sum11 += parseFloat(query.value(11));
190
+					sum11 += parseFloat(query.value(12));
178
 					sum5 += parseFloat(query.value(5));
191
 					sum5 += parseFloat(query.value(5));
179
-					sum12 += parseFloat(query.value(12));
192
+					sum12 += parseFloat(query.value(13));
180
 					sum6 += parseFloat(query.value(6));
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
 				output.writeEndElement(); // tbody
202
 				output.writeEndElement(); // tbody
188
 				output.writeStartElement("tfoot");
203
 				output.writeStartElement("tfoot");
198
 				output.writeTextElement("td", sum12.toFixed(2));
213
 				output.writeTextElement("td", sum12.toFixed(2));
199
 				output.writeTextElement("td", sum6.toFixed(2));
214
 				output.writeTextElement("td", sum6.toFixed(2));
200
 				output.writeTextElement("td", sum13.toFixed(2));
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
 				output.writeTextElement("td", sum9.toFixed(2));
218
 				output.writeTextElement("td", sum9.toFixed(2));
202
 				output.writeTextElement("td", sum15.toFixed(2));
219
 				output.writeTextElement("td", sum15.toFixed(2));
203
 				output.writeTextElement("td", sum7.toFixed(2));
220
 				output.writeTextElement("td", sum7.toFixed(2));

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

1
 <window id="item_transactions">
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
 </window>
371
 </window>

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

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

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
             button.clicked.connect(function() {
65
             button.clicked.connect(function() {
66
                 q = "INSERT INTO ";
66
                 q = "INSERT INTO ";
67
                 q += (types.currentIndex == 0 ?
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
                 q += " VALUES ('now', ";
70
                 q += " VALUES ('now', ";
71
                 q = q + items.currentData();
71
                 q = q + items.currentData();
72
                 q = q + ", ";
72
                 q = q + ", ";
82
                     q = q + ")";
82
                     q = q + ")";
83
                 }
83
                 }
84
                 q += (types.currentIndex == 0 ?
84
                 q += (types.currentIndex == 0 ?
85
-                    ")" :
86
-                    ", '" + reason.text + "')");
85
+                    ", :user)" :
86
+                    ", '" + reason.text + "', :user)");
87
                 query = new QSqlQuery();
87
                 query = new QSqlQuery();
88
-                query.exec(q);
88
+                query.prepare(q);
89
+				query.bind(":user", Application.currentTypicaUser());
90
+				query.exec();
89
                 updateStatus();
91
                 updateStatus();
90
             });
92
             });
91
             items['currentIndexChanged(int)'].connect(updateStatus);
93
             items['currentIndexChanged(int)'].connect(updateStatus);

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

53
 			var submit = findChildObject(this, 'submit');
53
 			var submit = findChildObject(this, 'submit');
54
 			submit.clicked.connect(function() {
54
 			submit.clicked.connect(function() {
55
 				var query = new QSqlQuery();
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
 				query.bind(":time", dateField.text);
57
 				query.bind(":time", dateField.text);
58
 				if(customerField.text == "") {
58
 				if(customerField.text == "") {
59
 					query.bind(":customer", null);
59
 					query.bind(":customer", null);
62
 					query.bind(":customer", customerField.text);
62
 					query.bind(":customer", customerField.text);
63
 				}
63
 				}
64
 				var coffeesArray = sqlToArray(items.columnArray(0, 32));
64
 				var coffeesArray = sqlToArray(items.columnArray(0, 32));
65
+				query.bind(":user", Application.currentTypicaUser());
65
 				if(coffeesArray.length > 0)
66
 				if(coffeesArray.length > 0)
66
 				{
67
 				{
67
 					for(var i = 0; i < coffeesArray.length; i++)
68
 					for(var i = 0; i < coffeesArray.length; i++)

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

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
                 <button name="Roast Coffee" id="roast" type="push" />
13
                 <button name="Roast Coffee" id="roast" type="push" />
14
             </column>
14
             </column>
15
         </row>
15
         </row>
16
+		<row>
17
+			<column>
18
+				<button name="Manual Roasting Log Entry" id="manual" type="push" />
19
+			</column>
20
+		</row>
16
         <row>
21
         <row>
17
             <column>
22
             <column>
18
                 <button name="Purchase Green Coffee" id="green" type="push" />
23
                 <button name="Purchase Green Coffee" id="green" type="push" />
68
     <menu name="Database">
73
     <menu name="Database">
69
         <item id="resetconnection">Forget Connection Details</item>
74
         <item id="resetconnection">Forget Connection Details</item>
70
     </menu>
75
     </menu>
76
+	<menu name="Users">
77
+		<item id="switchuser">Switch User</item>
78
+		<item id="createuser">Create New Users</item>
79
+	</menu>
71
     <program>
80
     <program>
72
         var window = this;
81
         var window = this;
73
         var navigationwindow = window;
82
         var navigationwindow = window;
87
             QSettings.setValue("database/user", "");
96
             QSettings.setValue("database/user", "");
88
             QSettings.setValue("database/password", "");
97
             QSettings.setValue("database/password", "");
89
         });
98
         });
99
+		var manual = findChildObject(this, 'manual');
100
+		manual.clicked.connect(function() {
101
+			createWindow("manualLogEntry");
102
+		});
90
         var profilehistory = findChildObject(this, 'profilehistory');
103
         var profilehistory = findChildObject(this, 'profilehistory');
91
         profilehistory.clicked.connect(function() {
104
         profilehistory.clicked.connect(function() {
92
                 createWindow("profilehistory");
105
                 createWindow("profilehistory");
133
         var nrbutton = findChildObject(this, 'newroasted');
146
         var nrbutton = findChildObject(this, 'newroasted');
134
         nrbutton.clicked.connect(function() {
147
         nrbutton.clicked.connect(function() {
135
             var nrwindow = createWindow("newroasted");
148
             var nrwindow = createWindow("newroasted");
136
-            nrwindow.windowTitle = "New Roasted Coffee Item";
149
+            nrwindow.windowTitle = "Manage Roasted Coffee Items";
137
         });
150
         });
138
         var importb = findChildObject(this, 'target');
151
         var importb = findChildObject(this, 'target');
139
         importb.clicked.connect(function() {
152
         importb.clicked.connect(function() {
226
 		   Typica 1.6 and later. */
239
 		   Typica 1.6 and later. */
227
 		var DBCreateSampleRoasting = function() {
240
 		var DBCreateSampleRoasting = function() {
228
 			var query = new QSqlQuery;
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
 			query.exec("CREATE TABLE IF NOT EXISTS coffee_sample_items(arrival timestamp without time zone, vendor text, attribute_ids bigint[], attribute_values text[], item_id bigint) INHERITS (items)");
243
 			query.exec("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
 			query.exec("INSERT INTO TypicaFeatures (feature, enabled, version) VALUES('sample-roasting', TRUE, 1)");
244
 			query.exec("INSERT INTO TypicaFeatures (feature, enabled, version) VALUES('sample-roasting', TRUE, 1)");
232
 			query = query.invalidate();
245
 			query = query.invalidate();
272
 		/* Update trigger functions to make column names explicit */
285
 		/* Update trigger functions to make column names explicit */
273
 		var DBUpdateTriggers = function() {
286
 		var DBUpdateTriggers = function() {
274
 			var query = new QSqlQuery;
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
 			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");
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
 			query.exec("UPDATE TypicaFeatures SET version = 4 WHERE feature = 'base-features'");
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
 		var DBUpdateSpecification = function() {
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
 				DBCreateSampleRoasting();
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
     </program>
407
     </program>
348
 </window>
408
 </window>

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

1
 <window id="batchWindow">
1
 <window id="batchWindow">
2
+	<menu name="File">
3
+		<item id="print" shortcut="Ctrl+P">Print...</item>
4
+	</menu>
2
     <menu name="Batch">
5
     <menu name="Batch">
3
         <item id="new" shortcut="Ctrl+N">New Batch...</item>
6
         <item id="new" shortcut="Ctrl+N">New Batch...</item>
4
     </menu>
7
     </menu>
75
             </layout>
78
             </layout>
76
             <label>Specification Details</label>
79
             <label>Specification Details</label>
77
             <textarea id="specnotes" />
80
             <textarea id="specnotes" />
81
+            <layout type="horizontal">
82
+                <label>File ID:</label>
83
+                <line id="filenofield" writable="false" />
84
+            </layout>
78
             <stretch />
85
             <stretch />
79
         </layout>
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
     </layout>
94
     </layout>
81
     <program>
95
     <program>
82
         <![CDATA[
96
         <![CDATA[
107
             roastwt.maximumWidth = 80;
121
             roastwt.maximumWidth = 80;
108
             var scalesLayout = findChildObject(this, 'scales');
122
             var scalesLayout = findChildObject(this, 'scales');
109
             scalesLayout.spacing = 10;
123
             scalesLayout.spacing = 10;
124
+			var batchTag = findChildObject(this, 'batchTag');
110
             if(navigationwindow.loggingWindow.scales.length > 0) {
125
             if(navigationwindow.loggingWindow.scales.length > 0) {
111
                 for(var i = 0; i < navigationwindow.loggingWindow.scales.length; i++) {
126
                 for(var i = 0; i < navigationwindow.loggingWindow.scales.length; i++) {
112
                     var scale = navigationwindow.loggingWindow.scales[i];
127
                     var scale = navigationwindow.loggingWindow.scales[i];
378
                     lossspec.text = "";
393
                     lossspec.text = "";
379
                     specnotes.plainText = "";
394
                     specnotes.plainText = "";
380
                 }
395
                 }
381
-                roastestimate.text = "";
382
                 query = query.invalidate();
396
                 query = query.invalidate();
397
+				drawTag();
383
             });
398
             });
384
             var validateCapacity = function() {
399
             var validateCapacity = function() {
385
                 if(checkCapacity == "true") {
400
                 if(checkCapacity == "true") {
389
                 }
404
                 }
390
                 return true;
405
                 return true;
391
             }
406
             }
407
+            var duration = findChildObject(this, 'duration');
408
+            var timefield = findChildObject(this, 'time');
392
             profilebutton.clicked.connect(function() {
409
             profilebutton.clicked.connect(function() {
393
                 var proceed = false;
410
                 var proceed = false;
394
                 if(validateCapacity()) {
411
                 if(validateCapacity()) {
397
                     proceed = displayWarning(TTR("batchWindow", "Suspicious Input"),
414
                     proceed = displayWarning(TTR("batchWindow", "Suspicious Input"),
398
                     TTR("batchWindow", "Entered green coffee weight exceeds maximum batch size. Continue?"));
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
                 if(proceed) {
421
                 if(proceed) {
401
                     doLoadProfile();
422
                     doLoadProfile();
402
                 }
423
                 }
480
                     proceed = displayWarning(TTR("batchWindow", "Suspicious Input"),
501
                     proceed = displayWarning(TTR("batchWindow", "Suspicious Input"),
481
                     TTR("batchWindow", "Entered green coffee weight exceeds maximum batch size. Continue?"));
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
                 if(proceed) {
508
                 if(proceed) {
484
                     doNoProfile();
509
                     doNoProfile();
485
                 }
510
                 }
491
                 navigationwindow.loggingWindow.activateWindow();
516
                 navigationwindow.loggingWindow.activateWindow();
492
             }
517
             }
493
             var submitbutton = findChildObject(this, 'submit');
518
             var submitbutton = findChildObject(this, 'submit');
494
-            var timefield = findChildObject(this, 'time');
495
             var notes = findChildObject(this, 'annotation');
519
             var notes = findChildObject(this, 'annotation');
496
-            var duration = findChildObject(this, 'duration');
497
             var approval = findChildObject(this, 'approval');
520
             var approval = findChildObject(this, 'approval');
498
             approval.checked = true;
521
             approval.checked = true;
499
             var target = findChildObject(this, 'target');
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
             var checkSubmitEnable = function () {
528
             var checkSubmitEnable = function () {
501
                 if(roasted.currentIndex > 0) {
529
                 if(roasted.currentIndex > 0) {
502
                     if(timefield.text.length > 0) {
530
                     if(timefield.text.length > 0) {
503
                         if(duration.text.length > 0) {
531
                         if(duration.text.length > 0) {
504
                             if(batch.tempData.length > 0) {
532
                             if(batch.tempData.length > 0) {
505
                                 if(green.text.length > 0) {
533
                                 if(green.text.length > 0) {
506
-                                    return true;
534
+                                    if(greenCheck()) {
535
+                                        return true;
536
+                                    }
507
                                 }
537
                                 }
508
                             }
538
                             }
509
                         }
539
                         }
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
             var doSubmit = function() {
575
             var doSubmit = function() {
532
                 checkQuery = new QSqlQuery();
576
                 checkQuery = new QSqlQuery();
533
                 checkQuery.exec("SELECT 1 FROM machine WHERE id = " + selectedRoasterID);
577
                 checkQuery.exec("SELECT 1 FROM machine WHERE id = " + selectedRoasterID);
539
                     checkQuery.exec();
583
                     checkQuery.exec();
540
                 }
584
                 }
541
                 checkQuery = checkQuery.invalidate();
585
                 checkQuery = checkQuery.invalidate();
542
-                var q = "INSERT INTO files (id, name, type, note, file) VALUES(default, :name, 'profile', NULL, :data) RETURNING id";
543
                 query = new QSqlQuery();
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
                 q2 = q2 + table.columnArray(0, 32);
589
                 q2 = q2 + table.columnArray(0, 32);
554
                 q2 = q2 + ", ";
590
                 q2 = q2 + ", ";
555
                 for(var i = 0; table.data(i, 1, 0).value != ""; i++)
591
                 for(var i = 0; table.data(i, 1, 0).value != ""; i++)
567
                 q2 = q2 + selectedRoasterID;
603
                 q2 = q2 + selectedRoasterID;
568
                 q2 = q2 + ", :duration, :approval, NULL, NULL, NULL, NULL, '{";
604
                 q2 = q2 + ", :duration, :approval, NULL, NULL, NULL, NULL, '{";
569
                 q2 = q2 + fileno;
605
                 q2 = q2 + fileno;
570
-                q2 = q2 + "}')";
606
+                q2 = q2 + "}', :user) RETURNING time";
571
                 query2 = new QSqlQuery();
607
                 query2 = new QSqlQuery();
572
                 query2.prepare(q2);
608
                 query2.prepare(q2);
573
                 query2.bind(":time", timefield.text);
609
                 query2.bind(":time", timefield.text);
574
                 query2.bind(":annotation", notes.plainText);
610
                 query2.bind(":annotation", notes.plainText);
575
                 query2.bind(":duration", duration.text);
611
                 query2.bind(":duration", duration.text);
576
                 query2.bind(":approval", approval.checked);
612
                 query2.bind(":approval", approval.checked);
613
+				query2.bind(":user", Application.currentTypicaUser());
577
                 query2.exec();
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
                 query2 = query2.invalidate();
621
                 query2 = query2.invalidate();
579
                 if(target.checked) {
622
                 if(target.checked) {
580
                     var q3 = "INSERT INTO item_files (time, item, files) VALUES(:time, :item, '{";
623
                     var q3 = "INSERT INTO item_files (time, item, files) VALUES(:time, :item, '{";
589
                 batch.windowModified = false;
632
                 batch.windowModified = false;
590
                 batch.close();
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
     </program>
702
     </program>
594
 </window>
703
 </window>

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

132
 			newMenu.triggered.connect(function() {
132
 			newMenu.triggered.connect(function() {
133
 				createWindow("sampleRoastingBatch");
133
 				createWindow("sampleRoastingBatch");
134
 			});
134
 			});
135
+            this.endBatch = function() {};
135
 			var batch = this;
136
 			var batch = this;
136
 			batch.submitButton = submit;
137
 			batch.submitButton = submit;
137
 			var name = findChildObject(this, 'name');
138
 			var name = findChildObject(this, 'name');
179
 			stop.clicked.connect(function() {
180
 			stop.clicked.connect(function() {
180
 				submit.setEnabled(true);
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
                                 TTR("sampleRoastingBatch", "Entered green coffee weight exceeds maximum batch size. Continue?"));
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
 				var lc = 1;
204
 				var lc = 1;
204
 				currentBatchInfo = batch;
205
 				currentBatchInfo = batch;
205
 				query = new QSqlQuery();
206
 				query = new QSqlQuery();
270
 			var vendor = findChildObject(this, 'vendor');
271
 			var vendor = findChildObject(this, 'vendor');
271
 			var attributes = findChildObject(this, 'attributes');
272
 			var attributes = findChildObject(this, 'attributes');
272
 			var target = findChildObject(this, 'target');
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
                                 TTR("sampleRoastingBatch", "Entered green coffee weight exceeds maximum batch size. Continue?"));
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
 				query = new QSqlQuery();
287
 				query = new QSqlQuery();
287
 				query.prepare("INSERT INTO files (id, name, type, note, file) VALUES(DEFAULT, :name, 'profile', NULL, :data) RETURNING id");
288
 				query.prepare("INSERT INTO files (id, name, type, note, file) VALUES(DEFAULT, :name, 'profile', NULL, :data) RETURNING id");
288
 				query.bind(":name", timefield.text + " " + name.text + " " + profileName.currentText);
289
 				query.bind(":name", timefield.text + " " + name.text + " " + profileName.currentText);
346
 				query.exec();
347
 				query.exec();
347
 				query.next();
348
 				query.next();
348
 				var roastedId = query.value(0);
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
 				query.bind(":time", timefield.text);
351
 				query.bind(":time", timefield.text);
351
 				query.bind(":unroastedids", "{" + greenId + "}");
352
 				query.bind(":unroastedids", "{" + greenId + "}");
352
                                 query.bind(":greens", "{" + convertToPounds(parseFloat(green.text), GunitBox.currentText) + "}");
353
                                 query.bind(":greens", "{" + convertToPounds(parseFloat(green.text), GunitBox.currentText) + "}");
357
 				query.bind(":machine", Number(selectedRoasterID));
358
 				query.bind(":machine", Number(selectedRoasterID));
358
 				query.bind(":duration", duration.text);
359
 				query.bind(":duration", duration.text);
359
 				query.bind(":files", "{" + fileno + "}");
360
 				query.bind(":files", "{" + fileno + "}");
361
+				query.bind(":user", Application.currentTypicaUser());
360
 				query.exec();
362
 				query.exec();
361
 				query = query.invalidate();
363
 				query = query.invalidate();
362
 				batch.close();
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
                     query.exec();
317
                     query.exec();
318
                     query.next();
318
                     query.next();
319
                     var item_id = query.value(0);
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
                     query.prepare(q);
321
                     query.prepare(q);
322
                     query.bind(":time", dateField.date);
322
                     query.bind(":time", dateField.date);
323
                     query.bind(":item", item_id);
323
                     query.bind(":item", item_id);
332
                         query.bind(":cost", Number(costEntry.text) / convertToPounds(parseFloat(quantityEntry.text), unitEntry.currentText));
332
                         query.bind(":cost", Number(costEntry.text) / convertToPounds(parseFloat(quantityEntry.text), unitEntry.currentText));
333
                     }
333
                     }
334
                     query.bind(":vendor", vendorField.currentText);
334
                     query.bind(":vendor", vendorField.currentText);
335
+					query.bind(":user", Application.currentTypicaUser());
335
                     query.exec();
336
                     query.exec();
336
                     q = "INSERT INTO lb_bag_conversion (item, conversion) VALUES(:item, :conversion)";
337
                     q = "INSERT INTO lb_bag_conversion (item, conversion) VALUES(:item, :conversion)";
337
                     query.prepare(q);
338
                     query.prepare(q);

+ 1
- 0
config/config.xml View File

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

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


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

135
     // (USB-serial, bluetooth-serial, 18F PICs, and so on)
135
     // (USB-serial, bluetooth-serial, 18F PICs, and so on)
136
     // if you know an other name prefix for serial ports please let us know
136
     // if you know an other name prefix for serial ports please let us know
137
     portNamePrefixes.clear();
137
     portNamePrefixes.clear();
138
-    portNamePrefixes << QLatin1String("ttyACM*") << QLatin1String("ttyUSB*") << QLatin1String("rfcomm*");
138
+    portNamePrefixes << QLatin1String("ttyACM*") << QLatin1String("ttyUSB*") << QLatin1String("rfcomm*") << QLatin1String("tnt*");
139
     portNameList += dir.entryList(portNamePrefixes, (QDir::System | QDir::Files), QDir::Name);
139
     portNameList += dir.entryList(portNamePrefixes, (QDir::System | QDir::Files), QDir::Name);
140
 
140
 
141
     foreach (QString str , portNameList) {
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
     scale.h \
26
     scale.h \
27
     draglabel.h \
27
     draglabel.h \
28
     daterangeselector.h \
28
     daterangeselector.h \
29
-    licensewindow.h
29
+    licensewindow.h \
30
+    printerselector.h
30
 SOURCES += typica.cpp \
31
 SOURCES += typica.cpp \
31
     helpmenu.cpp \
32
     helpmenu.cpp \
32
     abouttypica.cpp \
33
     abouttypica.cpp \
36
     scale.cpp \
37
     scale.cpp \
37
     draglabel.cpp \
38
     draglabel.cpp \
38
     daterangeselector.cpp \
39
     daterangeselector.cpp \
39
-    licensewindow.cpp
40
+    licensewindow.cpp \
41
+    printerselector.cpp
40
 
42
 
41
 RESOURCES += \
43
 RESOURCES += \
42
     resources.qrc
44
     resources.qrc
46
 QMAKE_INFO_PLIST = resources/Info.plist
48
 QMAKE_INFO_PLIST = resources/Info.plist
47
 
49
 
48
 CODECFORTR = UTF-8
50
 CODECFORTR = UTF-8
51
+TRANSLATIONS = Translations/Typica_de.ts

+ 5
- 5
src/abouttypica.cpp View File

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

+ 2
- 2
src/abouttypica.h View File

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

+ 20
- 20
src/daterangeselector.cpp View File

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

+ 4
- 4
src/daterangeselector.h View File

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

+ 1
- 1
src/daterangeselector.w View File

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

+ 2
- 2
src/draglabel.cpp View File

1
-/*1002:*/
1
+/*1054:*/
2
 #line 33 "./scales.w"
2
 #line 33 "./scales.w"
3
 
3
 
4
 #include "draglabel.h"
4
 #include "draglabel.h"
26
 }
26
 }
27
 }
27
 }
28
 
28
 
29
-/*:1002*/
29
+/*:1054*/

+ 2
- 2
src/draglabel.h View File

1
-/*1001:*/
1
+/*1053:*/
2
 #line 13 "./scales.w"
2
 #line 13 "./scales.w"
3
 
3
 
4
 #ifndef TypicaDragLabelInclude
4
 #ifndef TypicaDragLabelInclude
17
 
17
 
18
 #endif
18
 #endif
19
 
19
 
20
-/*:1001*/
20
+/*:1053*/

+ 5
- 1
src/graphsettings.w View File

62
 @ The constructor sets up the interface and restores any previous values from
62
 @ The constructor sets up the interface and restores any previous values from
63
 settings.
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
 @<GraphSettingsWidget implementation@>=
69
 @<GraphSettingsWidget implementation@>=
66
 GraphSettingsRelativeTab::GraphSettingsRelativeTab() : QWidget(NULL),
70
 GraphSettingsRelativeTab::GraphSettingsRelativeTab() : QWidget(NULL),
67
 	colorEdit(new QLineEdit)
71
 	colorEdit(new QLineEdit)
101
 	QHBoxLayout *axisLayout = new QHBoxLayout;
105
 	QHBoxLayout *axisLayout = new QHBoxLayout;
102
 	QLabel *axisLabel = new QLabel(tr("Grid line positions (comma separated):"));
106
 	QLabel *axisLabel = new QLabel(tr("Grid line positions (comma separated):"));
103
 	QLineEdit *axisEdit = new QLineEdit;
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
 	updateAxisSetting(axisEdit->text());
109
 	updateAxisSetting(axisEdit->text());
106
 	connect(axisEdit, SIGNAL(textChanged(QString)), this, SLOT(updateAxisSetting(QString)));
110
 	connect(axisEdit, SIGNAL(textChanged(QString)), this, SLOT(updateAxisSetting(QString)));
107
 	axisLayout->addWidget(axisLabel);
111
 	axisLayout->addWidget(axisLabel);

+ 7
- 7
src/helpmenu.cpp View File

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

+ 2
- 2
src/helpmenu.h View File

1
-/*203:*/
1
+/*206:*/
2
 #line 16 "./helpmenu.w"
2
 #line 16 "./helpmenu.w"
3
 
3
 
4
 #include <QMenu> 
4
 #include <QMenu> 
18
 
18
 
19
 #endif
19
 #endif
20
 
20
 
21
-/*:203*/
21
+/*:206*/

+ 10
- 10
src/licensewindow.cpp View File

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

+ 2
- 2
src/licensewindow.h View File

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

+ 285
- 0
src/mergeseries.w View File

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
        6,       // revision
22
        6,       // revision
23
        0,       // classname
23
        0,       // classname
24
        0,    0, // classinfo
24
        0,    0, // classinfo
25
-       4,   14, // methods
25
+       6,   14, // methods
26
        0,    0, // properties
26
        0,    0, // properties
27
        0,    0, // enums/sets
27
        0,    0, // enums/sets
28
        0,    0, // constructors
28
        0,    0, // constructors
35
  // slots: signature, parameters, type, tag, flags
35
  // slots: signature, parameters, type, tag, flags
36
       60,   12,   12,   12, 0x0a,
36
       60,   12,   12,   12, 0x0a,
37
       67,   12,   12,   12, 0x0a,
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
        0        // eod
42
        0        // eod
41
 };
43
 };
43
 static const char qt_meta_stringdata_SerialScale[] = {
45
 static const char qt_meta_stringdata_SerialScale[] = {
44
     "SerialScale\0\0weight,unit\0"
46
     "SerialScale\0\0weight,unit\0"
45
     "newMeasurement(double,Units::Unit)\0"
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
 void SerialScale::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
53
 void SerialScale::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
55
         case 0: _t->newMeasurement((*reinterpret_cast< double(*)>(_a[1])),(*reinterpret_cast< Units::Unit(*)>(_a[2]))); break;
59
         case 0: _t->newMeasurement((*reinterpret_cast< double(*)>(_a[1])),(*reinterpret_cast< Units::Unit(*)>(_a[2]))); break;
56
         case 1: _t->tare(); break;
60
         case 1: _t->tare(); break;
57
         case 2: _t->weigh(); break;
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
         default: ;
65
         default: ;
60
         }
66
         }
61
     }
67
     }
93
     if (_id < 0)
99
     if (_id < 0)
94
         return _id;
100
         return _id;
95
     if (_c == QMetaObject::InvokeMetaMethod) {
101
     if (_c == QMetaObject::InvokeMetaMethod) {
96
-        if (_id < 4)
102
+        if (_id < 6)
97
             qt_static_metacall(this, _c, _id, _a);
103
             qt_static_metacall(this, _c, _id, _a);
98
-        _id -= 4;
104
+        _id -= 6;
99
     }
105
     }
100
     return _id;
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

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

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

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

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

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

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

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">
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
 <html xmlns="http://www.w3.org/1999/xhtml">
2
 <html xmlns="http://www.w3.org/1999/xhtml">
3
 	<head>
3
 	<head>
4
-		<title>Typica - Data for Coffee Roasters</title>
4
+		<title>Typica - Software for Coffee Roasting Operations</title>
5
 		<link rel="stylesheet" type="text/css" href="style.css" />
5
 		<link rel="stylesheet" type="text/css" href="style.css" />
6
 	</head>
6
 	</head>
7
 	<body>
7
 	<body>
8
 		<div id="page">
8
 		<div id="page">
9
 			<div id="topmatter">
9
 			<div id="topmatter">
10
 				<div class="topbanner">
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
 				</div>
14
 				</div>
15
 			<div id="maintext">
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
 					<span class="icons">
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
 						<a href="https://twitter.com/N3Roaster">&#62217;</a>
19
 						<a href="https://twitter.com/N3Roaster">&#62217;</a>
20
 						<a href="https://github.com/N3Roaster">&#62208;</a>
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
 					</span>
21
 					</span>
24
 				</p>
22
 				</p>
25
 				<p>German Translation: Mario Champignon
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
 				<h3>License Information</h3>
29
 				<h3>License Information</h3>
32
 				<p>Permission is hereby granted, free of charge, to any person
30
 				<p>Permission is hereby granted, free of charge, to any person
93
 					<li>The name Michael Bostock may not be used to endorse or promote products derived from this software without specific prior written permission.
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
 				</ul>
92
 				</ul>
95
 				<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>
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
 			</div>
101
 			</div>
98
 		</div>
102
 		</div>
99
 	</body>
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
-<?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
-/*1008:*/
2
-#line 131 "./scales.w"
1
+/*1060:*/
2
+#line 135 "./scales.w"
3
 
3
 
4
 #include "scale.h"
4
 #include "scale.h"
5
 #include <QStringList> 
5
 #include <QStringList> 
10
 connect(this,SIGNAL(readyRead()),this,SLOT(dataAvailable()));
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
 void SerialScale::dataAvailable()
16
 void SerialScale::dataAvailable()
17
 {
17
 {
24
 }
24
 }
25
 else
25
 else
26
 {
26
 {
27
-/*1010:*/
28
-#line 189 "./scales.w"
27
+/*1062:*/
28
+#line 193 "./scales.w"
29
 
29
 
30
 QStringList responseParts= QString(responseBuffer.simplified()).split(' ');
30
 QStringList responseParts= QString(responseBuffer.simplified()).split(' ');
31
 if(responseParts.size()> 2)
31
 if(responseParts.size()> 2)
35
 }
35
 }
36
 double weight= responseParts[0].toDouble();
36
 double weight= responseParts[0].toDouble();
37
 Units::Unit unit= Units::Unitless;
37
 Units::Unit unit= Units::Unitless;
38
-if(responseParts[1]=="lb")
38
+if(responseParts[1].compare("lb",Qt::CaseInsensitive)==0)
39
 {
39
 {
40
 unit= Units::Pound;
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
 unit= Units::Kilogram;
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
 unit= Units::Gram;
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
 unit= Units::Ounce;
52
 unit= Units::Ounce;
53
 }
53
 }
54
 emit newMeasurement(weight,unit);
54
 emit newMeasurement(weight,unit);
55
 
55
 
56
-/*:1010*/
57
-#line 161 "./scales.w"
56
+/*:1062*/
57
+#line 165 "./scales.w"
58
 
58
 
59
 responseBuffer.clear();
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
 void SerialScale::tare()
67
 void SerialScale::tare()
68
 {
68
 {
71
 
71
 
72
 void SerialScale::weigh()
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
-/*1007:*/
1
+/*1059:*/
2
 #line 103 "./scales.w"
2
 #line 103 "./scales.w"
3
 
3
 
4
 #ifndef TypicaScaleInclude
4
 #ifndef TypicaScaleInclude
15
 public slots:
15
 public slots:
16
 void tare();
16
 void tare();
17
 void weigh();
17
 void weigh();
18
+void setWeighCommand(const QString&command);
19
+void setCommandTerminator(const QString&terminator);
18
 signals:
20
 signals:
19
 void newMeasurement(double weight,Units::Unit unit);
21
 void newMeasurement(double weight,Units::Unit unit);
20
 private slots:
22
 private slots:
21
 void dataAvailable();
23
 void dataAvailable();
22
 private:
24
 private:
23
 QByteArray responseBuffer;
25
 QByteArray responseBuffer;
26
+QByteArray weighCommand;
27
+QByteArray commandTerminator;
24
 };
28
 };
25
 
29
 
26
 #endif
30
 #endif
27
 
31
 
28
-/*:1007*/
32
+/*:1059*/

+ 68
- 6
src/scales.w View File

115
 	public slots:
115
 	public slots:
116
 		void tare();
116
 		void tare();
117
 		void weigh();
117
 		void weigh();
118
+		void setWeighCommand(const QString &command);
119
+		void setCommandTerminator(const QString &terminator);
118
 	signals:
120
 	signals:
119
 		void newMeasurement(double weight, Units::Unit unit);
121
 		void newMeasurement(double weight, Units::Unit unit);
120
 	private slots:
122
 	private slots:
121
 		void dataAvailable();
123
 		void dataAvailable();
122
 	private:
124
 	private:
123
 		QByteArray responseBuffer;
125
 		QByteArray responseBuffer;
126
+		QByteArray weighCommand;
127
+		QByteArray commandTerminator;
124
 };
128
 };
125
 
129
 
126
 #endif
130
 #endif
195
 }
199
 }
196
 double weight = responseParts[0].toDouble();
200
 double weight = responseParts[0].toDouble();
197
 Units::Unit unit = Units::Unitless;
201
 Units::Unit unit = Units::Unitless;
198
-if(responseParts[1] == "lb")
202
+if(responseParts[1].compare("lb", Qt::CaseInsensitive) == 0)
199
 {
203
 {
200
 	unit = Units::Pound;
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
 	unit = Units::Kilogram;
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
 	unit = Units::Gram;
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
 	unit = Units::Ounce;
216
 	unit = Units::Ounce;
213
 }
217
 }
225
 
229
 
226
 void SerialScale::weigh()
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
 @ This must be available to the host environment.
257
 @ This must be available to the host environment.
380
 		void updateParity(int index);
406
 		void updateParity(int index);
381
 		void updateFlowControl(int index);
407
 		void updateFlowControl(int index);
382
 		void updateStopBits(int index);
408
 		void updateStopBits(int index);
409
+		void updateWeighCommand(const QString &command);
410
+		void updateCommandTerminator(const QString &terminator);
383
 	private:
411
 	private:
384
 		PortSelector *port;
412
 		PortSelector *port;
385
 		BaudSelector *baud;
413
 		BaudSelector *baud;
386
 		ParitySelector *parity;
414
 		ParitySelector *parity;
387
 		FlowSelector *flow;
415
 		FlowSelector *flow;
388
 		StopSelector *stop;
416
 		StopSelector *stop;
417
+		QLineEdit *weighcommand;
418
+		QComboBox *commandterminator;
389
 };
419
 };
390
 
420
 
391
 @ This is very similar to other configuration widgets.
421
 @ This is very similar to other configuration widgets.
395
                                              const QModelIndex &index)
425
                                              const QModelIndex &index)
396
 : BasicDeviceConfigurationWidget(model, index),
426
 : BasicDeviceConfigurationWidget(model, index),
397
   port(new PortSelector), baud(new BaudSelector), parity(new ParitySelector),
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
 	QFormLayout *layout = new QFormLayout;
431
 	QFormLayout *layout = new QFormLayout;
401
 	layout->addRow(tr("Port:"), port);
432
 	layout->addRow(tr("Port:"), port);
415
 	layout->addRow(tr("Stop Bits:"), stop);
446
 	layout->addRow(tr("Stop Bits:"), stop);
416
 	connect(stop, SIGNAL(currentIndexChanged(int)),
447
 	connect(stop, SIGNAL(currentIndexChanged(int)),
417
 	        this, SLOT(updateStopBits(int)));
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
 	@<Get device configuration data for current node@>@;
459
 	@<Get device configuration data for current node@>@;
419
 	for(int i = 0; i < configData.size(); i++)
460
 	for(int i = 0; i < configData.size(); i++)
420
 	{
461
 	{
448
 		{
489
 		{
449
 			stop->setCurrentIndex(stop->findData(node.attribute("value")));
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
 	updatePort(port->currentText());
502
 	updatePort(port->currentText());
453
 	updateBaudRate(baud->currentText());
503
 	updateBaudRate(baud->currentText());
454
 	updateParity(parity->currentIndex());
504
 	updateParity(parity->currentIndex());
455
 	updateFlowControl(flow->currentIndex());
505
 	updateFlowControl(flow->currentIndex());
456
 	updateStopBits(stop->currentIndex());
506
 	updateStopBits(stop->currentIndex());
507
+	updateWeighCommand(weighcommand->text());
508
+	updateCommandTerminator(commandterminator->currentText());
457
 	setLayout(layout);
509
 	setLayout(layout);
458
 }
510
 }
459
 
511
 
485
 	updateAttribute("stopbits", stop->itemData(index).toString());
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
 @ The configuration widget is registered with the configuration system.
550
 @ The configuration widget is registered with the configuration system.
489
 
551
 
490
 @<Register device configuration widgets@>=
552
 @<Register device configuration widgets@>=

+ 220
- 0
src/thresholdannotation.w View File

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
 #include <winver.h>
1
 #include <winver.h>
2
 VS_VERSION_INFO	VERSIONINFO
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
 FILEFLAGSMASK	0x3fL
5
 FILEFLAGSMASK	0x3fL
6
 #ifdef _DEBUG
6
 #ifdef _DEBUG
7
 	FILEFLAGS	VS_FF_DEBUG
7
 	FILEFLAGS	VS_FF_DEBUG
16
 		BLOCK "040904b0"
16
 		BLOCK "040904b0"
17
 		BEGIN
17
 		BEGIN
18
 			VALUE "CompanyName", "Wilson's Coffee & Tea\0"
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
 			VALUE "InternalName", "Typica\0"
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
 			VALUE "OriginalFilename", "Typica.exe\0"
23
 			VALUE "OriginalFilename", "Typica.exe\0"
24
 			VALUE "ProductName", "Typica\0"
24
 			VALUE "ProductName", "Typica\0"
25
-			VALUE "ProductVersion", "1.7.0\0"
25
+			VALUE "ProductVersion", "1.8.0\0"
26
 		END
26
 		END
27
 	END
27
 	END
28
 	BLOCK "VarFileInfo"
28
 	BLOCK "VarFileInfo"
31
 	END
31
 	END
32
 END
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
 \mark{\noexpand\nullsec0{A Note on Notation}}
22
 \mark{\noexpand\nullsec0{A Note on Notation}}
23
 \def\pn{Typica}
23
 \def\pn{Typica}
24
 \def\filebase{typica}
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
 \def\title{\pn{} (Version \version)}
27
 \def\title{\pn{} (Version \version)}
28
 \newskip\dangerskipb
28
 \newskip\dangerskipb
29
 \newskip\dangerskip
29
 \newskip\dangerskip
609
 @<SerialScaleConfWidget implementation@>@/
609
 @<SerialScaleConfWidget implementation@>@/
610
 @<ValueAnnotation implementation@>@/
610
 @<ValueAnnotation implementation@>@/
611
 @<ValueAnnotationConfWidget implementation@>@/
611
 @<ValueAnnotationConfWidget implementation@>@/
612
+@<ModbusNG implementation@>@/
613
+@<ThresholdAnnotationConfWidget implementation@>@/
614
+@<Annotator implementation@>@/
612
 
615
 
613
 @ A few headers are required for various parts of \pn{}. These allow the use of
616
 @ A few headers are required for various parts of \pn{}. These allow the use of
614
 various Qt modules.
617
 various Qt modules.
807
 
810
 
808
 @<Function prototypes for scripting@>=
811
 @<Function prototypes for scripting@>=
809
 void setQObjectProperties(QScriptValue value, QScriptEngine *engine);
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
 @<Functions for scripting@>=
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
 @ The same can be done for |QPaintDevice| and |QLayoutItem|.
834
 @ The same can be done for |QPaintDevice| and |QLayoutItem|.
837
     /* Nothing needs to be done here. */
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
 @* Scripting QWidget.
888
 @* Scripting QWidget.
841
 
889
 
842
 \noindent The first interesting class in this hierarchy is |QWidget|. This is
890
 \noindent The first interesting class in this hierarchy is |QWidget|. This is
991
 Version 1.6 adds a new property for handling the |windowModified| property
1039
 Version 1.6 adds a new property for handling the |windowModified| property
992
 such that an appropriate prompt is provided to confirm or cancel close events.
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
 @<Class declarations@>=
1051
 @<Class declarations@>=
995
 class ScriptQMainWindow : public QMainWindow@/
1052
 class ScriptQMainWindow : public QMainWindow@/
996
 {@t\1@>@/
1053
 {@t\1@>@/
1004
         void saveSizeAndPosition(const QString &key);
1061
         void saveSizeAndPosition(const QString &key);
1005
         void restoreSizeAndPosition(const QString &key);
1062
         void restoreSizeAndPosition(const QString &key);
1006
         void displayStatus(const QString &message = QString());
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
     protected:@/
1069
     protected:@/
1009
         void closeEvent(QCloseEvent *event);
1070
         void closeEvent(QCloseEvent *event);
1010
         void showEvent(QShowEvent *event);@/
1071
         void showEvent(QShowEvent *event);@/
1011
-    signals:@/
1012
-        void aboutToClose(void);@/
1013
     private:@/
1072
     private:@/
1014
         QString cprompt;@t\2@>@/
1073
         QString cprompt;@t\2@>@/
1015
 }@t\kern-3pt@>;
1074
 }@t\kern-3pt@>;
1020
 ScriptQMainWindow::ScriptQMainWindow()@+: QMainWindow(NULL),
1079
 ScriptQMainWindow::ScriptQMainWindow()@+: QMainWindow(NULL),
1021
     cprompt(tr("Closing this window may result in loss of data. Continue?"))@/
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
 void ScriptQMainWindow::saveSizeAndPosition(const QString &key)
1092
 void ScriptQMainWindow::saveSizeAndPosition(const QString &key)
1070
     QMainWindow::show();
1136
     QMainWindow::show();
1071
 }
1137
 }
1072
 
1138
 
1139
+void ScriptQMainWindow::setupFinished()
1140
+{
1141
+	emit windowReady();
1142
+}
1143
+
1073
 @ When a close event occurs, we check the |windowModified| property to
1144
 @ When a close event occurs, we check the |windowModified| property to
1074
 determine if closing the window could result in loss of data. If this is
1145
 determine if closing the window could result in loss of data. If this is
1075
 true, we allow the event to be cancelled. Otherwise, a signal is emitted which
1146
 true, we allow the event to be cancelled. Otherwise, a signal is emitted which
4329
 the configuration file and load that instead of prompting for the information
4400
 the configuration file and load that instead of prompting for the information
4330
 if possible.
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
 @<Load the application configuration@>=
4406
 @<Load the application configuration@>=
4333
 QStringList arguments = QCoreApplication::arguments();
4407
 QStringList arguments = QCoreApplication::arguments();
4334
 int position = arguments.indexOf("-c");
4408
 int position = arguments.indexOf("-c");
4339
     {
4413
     {
4340
         filename = arguments.at(position + 1);
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
 if(filename.isEmpty())
4427
 if(filename.isEmpty())
4344
 {
4428
 {
4363
     {
4447
     {
4364
         app.configuration()->setContent(&file, true);
4448
         app.configuration()->setContent(&file, true);
4365
     }
4449
     }
4450
+} else {
4451
+	return 1;
4366
 }
4452
 }
4367
 @<Substitute included fragments@>@;
4453
 @<Substitute included fragments@>@;
4368
 
4454
 
4533
                          QStack<QLayout *> *layoutStack);
4619
                          QStack<QLayout *> *layoutStack);
4534
 void addSpinBoxToLayout(QDomElement element, QStack<QWidget *> *widgetStack,
4620
 void addSpinBoxToLayout(QDomElement element, QStack<QWidget *> *widgetStack,
4535
                         QStack<QLayout *> *layoutStack);
4621
                         QStack<QLayout *> *layoutStack);
4622
+void addTimeEditToLayout(QDomElement element, QStack<QWidget *> *widgetStack,
4623
+                         QStack<QLayout *> *layoutStack);
4536
 
4624
 
4537
 @ The functions for creating windows must be made available to the scripting
4625
 @ The functions for creating windows must be made available to the scripting
4538
 engine.
4626
 engine.
4641
 }
4729
 }
4642
 @<Insert help menu@>@;
4730
 @<Insert help menu@>@;
4643
 window->show();
4731
 window->show();
4732
+window->setupFinished();
4644
 
4733
 
4645
 @ Three element types make sense as top level children of a {\tt <window>}
4734
 @ Three element types make sense as top level children of a {\tt <window>}
4646
 element. An element representing a layout element can be used to apply that
4735
 element. An element representing a layout element can be used to apply that
4764
         {
4853
         {
4765
             menu->addSeparator();
4854
             menu->addSeparator();
4766
         }
4855
         }
4856
+        else if(itemElement.tagName() == "plugins")
4857
+        {
4858
+	        @<Process plugin item@>@;
4859
+        }
4767
     }
4860
     }
4768
     j++;
4861
     j++;
4769
 }
4862
 }
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
 @ Using a grid layout is a bit different from using a box layout. Child elements
5047
 @ Using a grid layout is a bit different from using a box layout. Child elements
4881
 with various attributes are required to take full advantage of this layout type.
5048
 with various attributes are required to take full advantage of this layout type.
4882
 All direct children of a grid layout element should be {\tt <row>} elements
5049
 All direct children of a grid layout element should be {\tt <row>} elements
5012
             {
5179
             {
5013
                 addCalendarToLayout(currentElement, widgetStack, layoutStack);
5180
                 addCalendarToLayout(currentElement, widgetStack, layoutStack);
5014
             }
5181
             }
5182
+            else if(currentElement.tagName() == "timeedit")
5183
+            {
5184
+	            addTimeEditToLayout(currentElement, widgetStack, layoutStack);
5185
+            }
5015
             else if(currentElement.tagName() == "decoration")
5186
             else if(currentElement.tagName() == "decoration")
5016
             {
5187
             {
5017
                 addDecorationToLayout(currentElement, widgetStack,
5188
                 addDecorationToLayout(currentElement, widgetStack,
6165
 QScriptValue QDateTimeEdit_year(QScriptContext *context, QScriptEngine *engine);
6336
 QScriptValue QDateTimeEdit_year(QScriptContext *context, QScriptEngine *engine);
6166
 QScriptValue QDateTimeEdit_setToCurrentTime(QScriptContext *context, QScriptEngine *engine);
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
 @ In order to get to objects created from the XML description, it is necessary
6376
 @ In order to get to objects created from the XML description, it is necessary
6169
 to provide a function that can be called to retrieve children of a given widget.
6377
 to provide a function that can be called to retrieve children of a given widget.
6170
 When providing such an object to the script, it is necessary to determine the
6378
 When providing such an object to the script, it is necessary to determine the
6304
 {
6512
 {
6305
     setQSvgWidgetProperties(value, engine);
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
 @ In the list of classes, the SaltTable entry is for a class which does not
6524
 @ In the list of classes, the SaltTable entry is for a class which does not
6309
 strictly exist on its own. It is, however, useful to provide some custom
6525
 strictly exist on its own. It is, however, useful to provide some custom
8219
     signals:@/
8435
     signals:@/
8220
         void timeForValue(double);
8436
         void timeForValue(double);
8221
     private:@/
8437
     private:@/
8438
+        bool previousValueValid;
8222
         double previousValue;
8439
         double previousValue;
8223
         double threshold;
8440
         double threshold;
8224
         EdgeDirection currentDirection;
8441
         EdgeDirection currentDirection;
8227
 @ This class emits the time in seconds when a given measurement crosses the
8444
 @ This class emits the time in seconds when a given measurement crosses the
8228
 threshold value in the appropriate direction.
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
 @<ThresholdDetector Implementation@>=
8454
 @<ThresholdDetector Implementation@>=
8231
 void ThresholdDetector::newMeasurement(Measurement measure)
8455
 void ThresholdDetector::newMeasurement(Measurement measure)
8232
 {
8456
 {
8233
     if((currentDirection == Ascending && previousValue < threshold &&
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
         if((currentDirection == Ascending && measure.temperature() >= threshold) ||
8461
         if((currentDirection == Ascending && measure.temperature() >= threshold) ||
8238
            (currentDirection == Descending && measure.temperature() <= threshold))
8462
            (currentDirection == Descending && measure.temperature() <= threshold))
8245
         }
8469
         }
8246
     }
8470
     }
8247
     previousValue = measure.temperature();
8471
     previousValue = measure.temperature();
8472
+    previousValueValid = true;
8248
 }
8473
 }
8249
 
8474
 
8250
 ThresholdDetector::ThresholdDetector(double value) : QObject(NULL),
8475
 ThresholdDetector::ThresholdDetector(double value) : QObject(NULL),
8476
+    previousValueValid(false),
8251
     previousValue(-1), threshold(value), currentDirection(Ascending)
8477
     previousValue(-1), threshold(value), currentDirection(Ascending)
8252
 {
8478
 {
8253
     /* Nothing needs to be done here. */
8479
     /* Nothing needs to be done here. */
8956
 Starting in version 1.4, column sizes are persisted automatically using the
9182
 Starting in version 1.4, column sizes are persisted automatically using the
8957
 same method as described in the section on |SqlQueryView|.
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
 @<Class declarations@>=
9188
 @<Class declarations@>=
8960
 class MeasurementModel;@/
9189
 class MeasurementModel;@/
8961
 class ZoomLog : public QTableView@/
9190
 class ZoomLog : public QTableView@/
8970
     public:@/
9199
     public:@/
8971
         ZoomLog();
9200
         ZoomLog();
8972
         QVariant data(int row, int column) const;
9201
         QVariant data(int row, int column) const;
8973
-        int rowCount();
9202
+        @[Q_INVOKABLE@,@, int rowCount();
8974
         bool saveXML(QIODevice *device);
9203
         bool saveXML(QIODevice *device);
8975
         bool saveCSV(QIODevice *device);
9204
         bool saveCSV(QIODevice *device);
8976
         QString lastTime(int series);
9205
         QString lastTime(int series);
9102
 measurements so that the |ZoomLog| thinks it gets at least one measurement
9331
 measurements so that the |ZoomLog| thinks it gets at least one measurement
9103
 every second.
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
 @<Synthesize measurements for slow hardware@>=
9340
 @<Synthesize measurements for slow hardware@>=
9112
 if(lastMeasurement.contains(tempcolumn))
9341
 if(lastMeasurement.contains(tempcolumn))
9114
     if(lastMeasurement[tempcolumn].time() < measure.time())
9343
     if(lastMeasurement[tempcolumn].time() < measure.time())
9115
     {
9344
     {
9116
         QList<QTime> timelist;
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
         for(QTime i = lastMeasurement.value(tempcolumn).time().addSecs(1); i < measure.time(); i = i.addSecs(1))
9352
         for(QTime i = lastMeasurement.value(tempcolumn).time().addSecs(1); i < measure.time(); i = i.addSecs(1))
9118
         {
9353
         {
9119
             timelist.append(i);
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
         for(int i = 0; i < timelist.size(); i++)
9358
         for(int i = 0; i < timelist.size(); i++)
9122
         {
9359
         {
9123
             Measurement synthesized = measure;
9360
             Measurement synthesized = measure;
9124
             synthesized.setTime(timelist[i]);
9361
             synthesized.setTime(timelist[i]);
9362
+            synthesized.setTemperature(templist[i]);
9125
             newMeasurement(synthesized, tempcolumn);
9363
             newMeasurement(synthesized, tempcolumn);
9126
         }
9364
         }
9127
     }
9365
     }
12569
 
12807
 
12570
 @i webview.w
12808
 @i webview.w
12571
 
12809
 
12810
+@i printerselector.w
12811
+
12572
 @* The Application class.
12812
 @* The Application class.
12573
 
12813
 
12574
 The |Application| class represents the \pn{} program. It is responsible for
12814
 The |Application| class represents the \pn{} program. It is responsible for
12589
         QDomDocument* configuration();
12829
         QDomDocument* configuration();
12590
         @<Device configuration members@>@;
12830
         @<Device configuration members@>@;
12591
         QSqlDatabase database();
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
         QScriptEngine *engine;@/
12836
         QScriptEngine *engine;@/
12593
     @[public slots@]:@/
12837
     @[public slots@]:@/
12838
+	    void setDatabaseConnected(bool status);
12839
+	    void setCurrentTypicaUser(const QString &user);
12594
         @<Extended Application slots@>@;
12840
         @<Extended Application slots@>@;
12841
+    @[signals@]:@/
12842
+	    void userChanged(const QString &user);
12595
     private:@/
12843
     private:@/
12596
         @<Application private data members@>@;
12844
         @<Application private data members@>@;
12597
         QDomDocument conf;
12845
         QDomDocument conf;
12846
+        bool connectionStatus;
12847
+        QString currentUser;
12598
 };
12848
 };
12599
 
12849
 
12600
 @ The constructor for this class handles a few things that had previously been
12850
 @ The constructor for this class handles a few things that had previously been
12601
 handled in |main()|.
12851
 handled in |main()|.
12602
 
12852
 
12603
 @<Application Implementation@>=
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
     @<Allow use of the default QSettings constructor@>@;
12857
     @<Allow use of the default QSettings constructor@>@;
12607
     @<Load translation objects@>@;
12858
     @<Load translation objects@>@;
12669
     return QSqlDatabase::cloneDatabase(connection, QString(connectionName));
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
 @** Table editor for ordered arrays with SQL relations.
12937
 @** Table editor for ordered arrays with SQL relations.
12673
 
12938
 
12674
 \noindent A database in use at Wilson's Coffee \char'046~Tea stores information
12939
 \noindent A database in use at Wilson's Coffee \char'046~Tea stores information
13495
     cancelButton(new QPushButton(tr("Cancel"))),
13760
     cancelButton(new QPushButton(tr("Cancel"))),
13496
     connectButton(new QPushButton(tr("Connect")))@/
13761
     connectButton(new QPushButton(tr("Connect")))@/
13497
 {
13762
 {
13763
+	QSettings settings;
13498
     driver->addItem("PostgreSQL", "QPSQL");
13764
     driver->addItem("PostgreSQL", "QPSQL");
13499
     formLayout->addRow(tr("Database driver:"), driver);
13765
     formLayout->addRow(tr("Database driver:"), driver);
13500
     formLayout->addRow(tr("Host name:"), hostname);
13766
     formLayout->addRow(tr("Host name:"), hostname);
13767
+    hostname->setText(settings.value("database/hostname").toString());
13501
     formLayout->addRow(tr("Port number:"), portnumber);
13768
     formLayout->addRow(tr("Port number:"), portnumber);
13502
-    portnumber->setText("5432");
13769
+    portnumber->setText(settings.value("database/portnumber", "5432").toString());
13503
     formLayout->addRow(tr("Database name:"), dbname);
13770
     formLayout->addRow(tr("Database name:"), dbname);
13771
+    dbname->setText(settings.value("database/dbname").toString());
13504
     formLayout->addRow(tr("User name:"), user);
13772
     formLayout->addRow(tr("User name:"), user);
13773
+    user->setText(settings.value("database/user").toString());
13505
     password->setEchoMode(QLineEdit::Password);
13774
     password->setEchoMode(QLineEdit::Password);
13506
     formLayout->addRow(tr("Password:"), password);
13775
     formLayout->addRow(tr("Password:"), password);
13776
+    password->setText(settings.value("database/password").toString());
13507
     layout->addLayout(formLayout);
13777
     layout->addLayout(formLayout);
13508
     buttons->addStretch(1);
13778
     buttons->addStretch(1);
13509
     buttons->addWidget(cancelButton);
13779
     buttons->addWidget(cancelButton);
13548
         settings.setValue("database/user", user->text());
13818
         settings.setValue("database/user", user->text());
13549
         settings.setValue("database/password", password->text());
13819
         settings.setValue("database/password", password->text());
13550
         database.close();
13820
         database.close();
13821
+        AppInstance->setDatabaseConnected(true);
13551
         accept();
13822
         accept();
13552
     }
13823
     }
13553
     else
13824
     else
13587
 else
13858
 else
13588
 {
13859
 {
13589
     database.close();
13860
     database.close();
13861
+    AppInstance->setDatabaseConnected(true);
13590
 }
13862
 }
13591
 
13863
 
13592
 @** Viewing a record of batches.
13864
 @** Viewing a record of batches.
14293
     value.setProperty("print", engine->newFunction(QTextEdit_print));
14565
     value.setProperty("print", engine->newFunction(QTextEdit_print));
14294
 }
14566
 }
14295
 
14567
 
14568
+@i plugins.w
14569
+
14296
 @i daterangeselector.w
14570
 @i daterangeselector.w
14297
 
14571
 
14298
 @** An area for repeated user interface elements.
14572
 @** An area for repeated user interface elements.
15325
         QModelIndex index(int row, int column,
15599
         QModelIndex index(int row, int column,
15326
                           const QModelIndex &parent = QModelIndex()) const;
15600
                           const QModelIndex &parent = QModelIndex()) const;
15327
         QModelIndex parent(const QModelIndex &child) const;
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
         int columnCount(const QModelIndex &parent = QModelIndex()) const;
15603
         int columnCount(const QModelIndex &parent = QModelIndex()) const;
15330
         bool setData(const QModelIndex &index, const QVariant &value,
15604
         bool setData(const QModelIndex &index, const QVariant &value,
15331
                      int role);
15605
                      int role);
17098
 
17372
 
17099
 @** Configuration of Serial Devices Using Modbus RTU.
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
 Modbus RTU. This protocol allows multiple devices to be chained together on a
17380
 Modbus RTU. This protocol allows multiple devices to be chained together on a
17103
 two wire bus which can be connected to a single serial port. The communication
17381
 two wire bus which can be connected to a single serial port. The communication
17104
 protocol involves a single message which is sent from a master device (in this
17382
 protocol involves a single message which is sent from a master device (in this
18171
 : QObject(NULL), messageDelayTimer(new QTimer), commTimeout(new QTimer), unitIsF(@[true@]), readingsv(@[false@]),
18449
 : QObject(NULL), messageDelayTimer(new QTimer), commTimeout(new QTimer), unitIsF(@[true@]), readingsv(@[false@]),
18172
     waiting(@[false@])@/
18450
     waiting(@[false@])@/
18173
 {@/
18451
 {@/
18174
-qDebug() << "Initializing Modbus RTU Device";
18175
     QDomElement portReferenceElement = model->referenceElement(model->data(index,
18452
     QDomElement portReferenceElement = model->referenceElement(model->data(index,
18176
         Qt::UserRole).toString());
18453
         Qt::UserRole).toString());
18177
     QDomNodeList portConfigData = portReferenceElement.elementsByTagName("attribute");
18454
     QDomNodeList portConfigData = portReferenceElement.elementsByTagName("attribute");
18657
         char *check = (char*)&crc;
18934
         char *check = (char*)&crc;
18658
         message.append(check[0]);
18935
         message.append(check[0]);
18659
         message.append(check[1]);
18936
         message.append(check[1]);
18937
+        qDebug() << "Writing" << message.toHex();
18660
         port->write(message);
18938
         port->write(message);
18661
         commTimeout->start(2000);
18939
         commTimeout->start(2000);
18662
         messageDelayTimer->start(delayTime);
18940
         messageDelayTimer->start(delayTime);
19330
 inserter = new NodeInserter(tr("Modbus RTU Device"), tr("Modbus RTU Device"), "modbusrtu", NULL);
19608
 inserter = new NodeInserter(tr("Modbus RTU Device"), tr("Modbus RTU Device"), "modbusrtu", NULL);
19331
 topLevelNodeInserters.append(inserter);
19609
 topLevelNodeInserters.append(inserter);
19332
 
19610
 
19611
+@i modbus.w
19612
+
19333
 @i unsupportedserial.w
19613
 @i unsupportedserial.w
19334
 
19614
 
19335
 @i phidgets.w
19615
 @i phidgets.w
19898
     @[private slots@]:@/
20178
     @[private slots@]:@/
19899
         void updateMatchingColumn(const QString &column);
20179
         void updateMatchingColumn(const QString &column);
19900
         void updateTemperature();
20180
         void updateTemperature();
20181
+        void updateDelay();
19901
     private:@/
20182
     private:@/
19902
         QDoubleSpinBox *temperatureValue;
20183
         QDoubleSpinBox *temperatureValue;
19903
         QComboBox *unitSelector;
20184
         QComboBox *unitSelector;
20185
+        QSpinBox *delaySelector;
19904
 };
20186
 };
19905
 
20187
 
19906
 @ The constructor sets up our user interface.
20188
 @ The constructor sets up our user interface.
19908
 @<TranslationConfWidget implementation@>=
20190
 @<TranslationConfWidget implementation@>=
19909
 TranslationConfWidget::TranslationConfWidget(DeviceTreeModel *model, const QModelIndex &index)
20191
 TranslationConfWidget::TranslationConfWidget(DeviceTreeModel *model, const QModelIndex &index)
19910
 : BasicDeviceConfigurationWidget(model, index),
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
     unitSelector->addItem("Fahrenheit");
20196
     unitSelector->addItem("Fahrenheit");
19914
     unitSelector->addItem("Celsius");
20197
     unitSelector->addItem("Celsius");
19919
     layout->addRow(tr("Column to match:"), column);
20202
     layout->addRow(tr("Column to match:"), column);
19920
     layout->addRow(tr("Unit:"), unitSelector);
20203
     layout->addRow(tr("Unit:"), unitSelector);
19921
     layout->addRow(tr("Value:"), temperatureValue);
20204
     layout->addRow(tr("Value:"), temperatureValue);
20205
+    layout->addRow(tr("Start of batch safety delay:"), delaySelector);
19922
     @<Get device configuration data for current node@>@;
20206
     @<Get device configuration data for current node@>@;
19923
     for(int i = 0; i < configData.size(); i++)
20207
     for(int i = 0; i < configData.size(); i++)
19924
     {
20208
     {
19935
         {
20219
         {
19936
             temperatureValue->setValue(node.attribute("value").toDouble());
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
     updateMatchingColumn(column->text());
20227
     updateMatchingColumn(column->text());
19940
     updateTemperature();
20228
     updateTemperature();
20229
+    updateDelay();
19941
     connect(column, SIGNAL(textEdited(QString)), this, SLOT(updateMatchingColumn(QString)));
20230
     connect(column, SIGNAL(textEdited(QString)), this, SLOT(updateMatchingColumn(QString)));
19942
     connect(unitSelector, SIGNAL(currentIndexChanged(QString)), this, SLOT(updateTemperature()));
20231
     connect(unitSelector, SIGNAL(currentIndexChanged(QString)), this, SLOT(updateTemperature()));
19943
     connect(temperatureValue, SIGNAL(valueChanged(double)), this, SLOT(updateTemperature()));
20232
     connect(temperatureValue, SIGNAL(valueChanged(double)), this, SLOT(updateTemperature()));
20233
+    connect(delaySelector, SIGNAL(valueChanged(int)), this, SLOT(updateDelay()));
19944
     setLayout(layout);
20234
     setLayout(layout);
19945
 }
20235
 }
19946
 
20236
 
19968
     updateAttribute("column", column);
20258
     updateAttribute("column", column);
19969
 }
20259
 }
19970
 
20260
 
20261
+void TranslationConfWidget::updateDelay()
20262
+{
20263
+	updateAttribute("delay", QString("%1").arg(delaySelector->value()));
20264
+}
20265
+
19971
 @ This is registered with the configuration system.
20266
 @ This is registered with the configuration system.
19972
 
20267
 
19973
 @<Register device configuration widgets@>=
20268
 @<Register device configuration widgets@>=
19975
 
20270
 
19976
 @i rate.w
20271
 @i rate.w
19977
 
20272
 
20273
+@i mergeseries.w
20274
+
19978
 @i dataqsdk.w
20275
 @i dataqsdk.w
19979
 
20276
 
19980
 @i scales.w
20277
 @i scales.w
19981
 
20278
 
19982
 @i valueannotation.w
20279
 @i valueannotation.w
19983
 
20280
 
20281
+@i thresholdannotation.w
20282
+
20283
+@i user.w
20284
+
19984
 @** Local changes.
20285
 @** Local changes.
19985
 
20286
 
19986
 \noindent This is the end of \pn{} as distributed by its author. It is expected
20287
 \noindent This is the end of \pn{} as distributed by its author. It is expected

+ 6
- 6
src/units.cpp View File

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

+ 2
- 2
src/units.h View File

1
-/*278:*/
1
+/*290:*/
2
 #line 8 "./units.w"
2
 #line 8 "./units.w"
3
 
3
 
4
 #include <QObject> 
4
 #include <QObject> 
32
 
32
 
33
 #endif
33
 #endif
34
 
34
 
35
-/*:278*/
35
+/*:290*/

+ 0
- 38
src/unsupportedserial.w View File

692
 	return QScriptValue();
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

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
-/*573:*/
2
-#line 368 "./webview.w"
1
+/*586:*/
2
+#line 383 "./webview.w"
3
 
3
 
4
 #include "webelement.h"
4
 #include "webelement.h"
5
 
5
 
6
-/*571:*/
7
-#line 311 "./webview.w"
6
+/*584:*/
7
+#line 326 "./webview.w"
8
 
8
 
9
 TypicaWebElement::TypicaWebElement(QWebElement element):e(element)
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
 void TypicaWebElement::appendInside(const QString&markup)
17
 void TypicaWebElement::appendInside(const QString&markup)
18
 {
18
 {
59
 e.setPlainText(text);
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
-/*566:*/
2
-#line 248 "./webview.w"
1
+/*579:*/
2
+#line 263 "./webview.w"
3
 
3
 
4
 #include <QWebElement> 
4
 #include <QWebElement> 
5
 #include <QObject> 
5
 #include <QObject> 
27
 
27
 
28
 #endif
28
 #endif
29
 
29
 
30
-/*:566*/
30
+/*:579*/

+ 32
- 22
src/webview.cpp View File

1
-/*551:*/
1
+/*563:*/
2
 #line 50 "./webview.w"
2
 #line 50 "./webview.w"
3
 
3
 
4
 #include "webview.h"
4
 #include "webview.h"
5
 
5
 
6
-/*552:*/
6
+/*564:*/
7
 #line 57 "./webview.w"
7
 #line 57 "./webview.w"
8
 
8
 
9
 TypicaWebView::TypicaWebView():QWebView()
9
 TypicaWebView::TypicaWebView():QWebView()
12
 connect(page(),SIGNAL(linkClicked(QUrl)),this,SLOT(linkDelegate(QUrl)));
12
 connect(page(),SIGNAL(linkClicked(QUrl)),this,SLOT(linkDelegate(QUrl)));
13
 }
13
 }
14
 
14
 
15
-/*:552*//*553:*/
15
+/*:564*//*565:*/
16
 #line 73 "./webview.w"
16
 #line 73 "./webview.w"
17
 
17
 
18
 void TypicaWebView::linkDelegate(const QUrl&url)
18
 void TypicaWebView::linkDelegate(const QUrl&url)
20
 if(url.scheme()=="typica")
20
 if(url.scheme()=="typica")
21
 {
21
 {
22
 QString address(url.toEncoded());
22
 QString address(url.toEncoded());
23
-/*554:*/
23
+/*566:*/
24
 #line 91 "./webview.w"
24
 #line 91 "./webview.w"
25
 
25
 
26
 if(address=="typica://aboutqt")
26
 if(address=="typica://aboutqt")
29
 return;
29
 return;
30
 }
30
 }
31
 
31
 
32
-/*:554*/
32
+/*:566*/
33
 #line 79 "./webview.w"
33
 #line 79 "./webview.w"
34
 
34
 
35
-/*555:*/
35
+/*567:*/
36
 #line 100 "./webview.w"
36
 #line 100 "./webview.w"
37
 
37
 
38
 if(address.startsWith("typica://script/"))
38
 if(address.startsWith("typica://script/"))
41
 return;
41
 return;
42
 }
42
 }
43
 
43
 
44
-/*:555*/
44
+/*:567*/
45
 #line 80 "./webview.w"
45
 #line 80 "./webview.w"
46
 
46
 
47
 }
47
 }
51
 }
51
 }
52
 }
52
 }
53
 
53
 
54
-/*:553*//*556:*/
54
+/*:565*//*568:*/
55
 #line 112 "./webview.w"
55
 #line 112 "./webview.w"
56
 
56
 
57
 void TypicaWebView::load(const QString&url)
57
 void TypicaWebView::load(const QString&url)
59
 QWebView::load(QUrl(url));
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
 void TypicaWebView::setHtml(const QString&html,const QUrl&baseUrl)
62
 void TypicaWebView::setHtml(const QString&html,const QUrl&baseUrl)
73
 {
63
 {
74
 QWebView::setHtml(html,baseUrl);
64
 QWebView::setHtml(html,baseUrl);
88
 return page()->currentFrame()->documentElement().toOuterXml();
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
 QWebElement TypicaWebView::documentElement()
104
 QWebElement TypicaWebView::documentElement()
95
 {
105
 {
101
 return page()->mainFrame()->findFirstElement(selector);
111
 return page()->mainFrame()->findFirstElement(selector);
102
 }
112
 }
103
 
113
 
104
-/*:562*/
114
+/*:575*/
105
 #line 53 "./webview.w"
115
 #line 53 "./webview.w"
106
 
116
 
107
 
117
 
108
-/*:551*/
118
+/*:563*/

+ 3
- 3
src/webview.h View File

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

+ 26
- 11
src/webview.w View File

31
 	public:@/
31
 	public:@/
32
 		TypicaWebView();
32
 		TypicaWebView();
33
 		@[Q_INVOKABLE@,@, void@]@, load(const QString &url);@t\2\2@>@/
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
 		@[Q_INVOKABLE@,@, void@]@, setHtml(const QString &html, const QUrl &baseUrl = QUrl());@t\2\2@>@/
35
 		@[Q_INVOKABLE@,@, void@]@, setHtml(const QString &html, const QUrl &baseUrl = QUrl());@t\2\2@>@/
36
 		@[Q_INVOKABLE@,@, void@]@, setContent(QIODevice *device);@t\2\2@>@/
36
 		@[Q_INVOKABLE@,@, void@]@, setContent(QIODevice *device);@t\2\2@>@/
37
 		@[Q_INVOKABLE@,@, QString@]@, saveXml();@t\2\2@>@/
37
 		@[Q_INVOKABLE@,@, QString@]@, saveXml();@t\2\2@>@/
115
 	QWebView::load(QUrl(url));
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
 void TypicaWebView::setHtml(const QString &html, const QUrl &baseUrl)
118
 void TypicaWebView::setHtml(const QString &html, const QUrl &baseUrl)
129
 {
119
 {
130
 	QWebView::setHtml(html, baseUrl);
120
 	QWebView::setHtml(html, baseUrl);
144
 	return page()->currentFrame()->documentElement().toOuterXml();
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
 @ Web views are exposed to the host environment in the usual manner.
162
 @ Web views are exposed to the host environment in the usual manner.
148
 
163
 
149
 @<Set up the scripting engine@>=
164
 @<Set up the scripting engine@>=

+ 3
- 3
typica.desktop View File

1
 [Desktop Entry]
1
 [Desktop Entry]
2
-Version=1.6.3
2
+Version=1.8.0
3
 Name=Typica
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
 Exec=typica
6
 Exec=typica
7
 Icon=typica
7
 Icon=typica
8
 Terminal=false
8
 Terminal=false

BIN
typica.png View File


Loading…
Cancel
Save