Browse Source

Merge branch 'development' into release-1.6.1

Neal Wilson 11 years ago
parent
commit
d07854f7e3

+ 35
- 44
config/Reports/auco.xml View File

@@ -65,7 +65,7 @@
65 65
                 output.writeStartElement("thead");
66 66
                 output.writeStartElement("tr");
67 67
                 output.writeStartElement("th");
68
-                output.writeAttribute("colspan", "3");
68
+                output.writeAttribute("colspan", "5");
69 69
                 output.writeCharacters("Regular Coffees");
70 70
                 output.writeEndElement();
71 71
                 output.writeEndElement();
@@ -73,6 +73,8 @@
73 73
                 output.writeTextElement("th", "Origin");
74 74
                 output.writeTextElement("th", "Avg. Rate");
75 75
                 output.writeTextElement("th", "Avg. Cost");
76
+				output.writeTextElement("th", "Last Cost");
77
+				output.writeTextElement("th", "Last Purchase Date");
76 78
                 output.writeEndElement();
77 79
                 output.writeEndElement();
78 80
                 output.writeStartElement("tbody");
@@ -81,29 +83,32 @@
81 83
 				if(unitBox.currentIndex == 0) {
82 84
 					conversion = 2.2;
83 85
 				}
84
-                switch(sortBox.currentIndex)
85
-                {
86
-                    case 0:
87
-						query.prepare("SELECT DISTINCT origin, (avg(rate)/:conversion)::numeric(10,2) AS rate, (SELECT avg(cost)*:conversion2 FROM purchase WHERE item IN (SELECT id FROM regular_coffees WHERE origin = coffee_history.origin))::numeric(10,2) AS cost FROM coffee_history WHERE id IN (SELECT id FROM regular_coffees) GROUP BY origin ORDER BY origin ASC");
88
-                        break;
89
-                    case 1:
90
-						query.prepare("SELECT DISTINCT origin, (avg(rate)/:conversion)::numeric(10,2) AS rate, (SELECT avg(cost)*:conversion2 FROM purchase WHERE item IN (SELECT id FROM regular_coffees WHERE origin = coffee_history.origin))::numeric(10,2) AS cost FROM coffee_history WHERE id IN (SELECT id FROM regular_coffees) GROUP BY origin ORDER BY origin DESC");
91
-                        break;
92
-                    case 2:
93
-						query.prepare("SELECT DISTINCT origin, (avg(rate)/:conversion)::numeric(10,2) AS rate, (SELECT avg(cost)*:conversion2 FROM purchase WHERE item IN (SELECT id FROM regular_coffees WHERE origin = coffee_history.origin))::numeric(10,2) AS cost FROM coffee_history WHERE id IN (SELECT id FROM regular_coffees) GROUP BY origin ORDER BY rate ASC");
94
-                        break;
95
-                    case 3:
96
-						query.prepare("SELECT DISTINCT origin, (avg(rate)/:conversion)::numeric(10,2) AS rate, (SELECT avg(cost)*:conversion2 FROM purchase WHERE item IN (SELECT id FROM regular_coffees WHERE origin = coffee_history.origin))::numeric(10,2) AS cost FROM coffee_history WHERE id IN (SELECT id FROM regular_coffees) GROUP BY origin ORDER BY rate DESC");
97
-                        break;
98
-                    case 4:
99
-						query.prepare("SELECT DISTINCT origin, (avg(rate)/:conversion)::numeric(10,2) AS rate, (SELECT avg(cost)*:conversion2 FROM purchase WHERE item IN (SELECT id FROM regular_coffees WHERE origin = coffee_history.origin))::numeric(10,2) AS cost FROM coffee_history WHERE id IN (SELECT id FROM regular_coffees) GROUP BY origin ORDER BY cost ASC");
100
-                        break;
101
-                    case 5:
102
-						query.prepare("SELECT DISTINCT origin, (avg(rate)/:conversion)::numeric(10,2) AS rate, (SELECT avg(cost)*:conversion2 FROM purchase WHERE item IN (SELECT id FROM regular_coffees WHERE origin = coffee_history.origin))::numeric(10,2) AS cost FROM coffee_history WHERE id IN (SELECT id FROM regular_coffees) GROUP BY origin ORDER BY cost DESC");
103
-                        break;
104
-                }
86
+				var orderClause;
87
+				switch(sortBox.currentIndex)
88
+				{
89
+					case 0:
90
+						orderClause = "origin ASC";
91
+						break;
92
+					case 1:
93
+						orderClause = "origin DESC";
94
+						break;
95
+					case 2:
96
+						orderClause = "rate ASC";
97
+						break;
98
+					case 3:
99
+						orderClause = "rate DESC";
100
+						break;
101
+					case 4:
102
+						orderClause = "cost ASC";
103
+						break;
104
+					case 5:
105
+						orderClause = "cost DESC";
106
+						break;
107
+				}
108
+				query.prepare("SELECT DISTINCT origin, (avg(rate)/:conversion)::numeric(10,2) AS rate, (SELECT avg(cost)*:conversion2 FROM purchase WHERE item IN (SELECT id FROM regular_coffees WHERE origin = coffee_history.origin))::numeric(10,2) AS cost, (SELECT avg(cost)*:conversion3 FROM purchase WHERE item IN (SELECT id FROM regular_coffees WHERE origin = coffee_history.origin) AND time = (SELECT max(time) FROM purchase WHERE item IN (SELECT id FROM regular_coffees WHERE origin = coffee_history.origin))), (SELECT max(time)::date FROM purchase WHERE item IN (SELECT id FROM regular_coffees WHERE origin = coffee_history.origin)) FROM coffee_history WHERE id IN (SELECT id FROM regular_coffees) GROUP BY origin ORDER BY " + orderClause);
105 109
 				query.bind(":conversion", conversion);
106 110
 				query.bind(":conversion2", conversion);
111
+				query.bind(":conversion3", conversion);
107 112
 				query.exec();
108 113
                 while(query.next())
109 114
                 {
@@ -111,37 +116,20 @@
111 116
                     output.writeTextElement("td", query.value(0));
112 117
                     output.writeTextElement("td", query.value(1));
113 118
                     output.writeTextElement("td", query.value(2));
119
+					output.writeTextElement("td", query.value(3));
120
+					output.writeTextElement("td", query.value(4));
114 121
                     output.writeEndElement();
115 122
                 }
116 123
                 output.writeStartElement("tr");
117 124
                 output.writeStartElement("th");
118
-                output.writeAttribute("colspan", "3");
125
+                output.writeAttribute("colspan", "5");
119 126
                 output.writeCharacters("Decaffeinated Coffees");
120 127
                 output.writeEndElement();
121 128
                 output.writeEndElement();
122
-                switch(sortBox.currentIndex)
123
-                {
124
-                    case 0:
125
-						query.prepare("SELECT DISTINCT origin, (avg(rate)/:conversion)::numeric(10,2) AS rate, (SELECT avg(cost)*:conversion2 FROM purchase WHERE item IN (SELECT id FROM decaf_coffees WHERE origin = coffee_history.origin))::numeric(10,2) AS cost FROM coffee_history WHERE id IN (SELECT id FROM decaf_coffees) GROUP BY origin ORDER BY origin ASC");
126
-                        break;
127
-                    case 1:
128
-						query.prepare("SELECT DISTINCT origin, (avg(rate)/:conversion)::numeric(10,2) AS rate, (SELECT avg(cost)*:conversion2 FROM purchase WHERE item IN (SELECT id FROM decaf_coffees WHERE origin = coffee_history.origin))::numeric(10,2) AS cost FROM coffee_history WHERE id IN (SELECT id FROM decaf_coffees) GROUP BY origin ORDER BY origin DESC");
129
-                        break;
130
-                    case 2:
131
-						query.prepare("SELECT DISTINCT origin, (avg(rate)/:conversion)::numeric(10,2) AS rate, (SELECT avg(cost)*:conversion2 FROM purchase WHERE item IN (SELECT id FROM decaf_coffees WHERE origin = coffee_history.origin))::numeric(10,2) AS cost FROM coffee_history WHERE id IN (SELECT id FROM decaf_coffees) GROUP BY origin ORDER BY rate ASC");
132
-                        break;
133
-                    case 3:
134
-						query.prepare("SELECT DISTINCT origin, (avg(rate)/:conversion)::numeric(10,2) AS rate, (SELECT avg(cost)*:conversion2 FROM purchase WHERE item IN (SELECT id FROM decaf_coffees WHERE origin = coffee_history.origin))::numeric(10,2) AS cost FROM coffee_history WHERE id IN (SELECT id FROM decaf_coffees) GROUP BY origin ORDER BY rate DESC");
135
-                        break;
136
-                    case 4:
137
-						query.prepare("SELECT DISTINCT origin, (avg(rate)/:conversion)::numeric(10,2) AS rate, (SELECT avg(cost)*:conversion2 FROM purchase WHERE item IN (SELECT id FROM decaf_coffees WHERE origin = coffee_history.origin))::numeric(10,2) AS cost FROM coffee_history WHERE id IN (SELECT id FROM decaf_coffees) GROUP BY origin ORDER BY cost ASC");
138
-                        break;
139
-                    case 5:
140
-						query.prepare("SELECT DISTINCT origin, (avg(rate)/:conversion)::numeric(10,2) AS rate, (SELECT avg(cost)*:conversion2 FROM purchase WHERE item IN (SELECT id FROM decaf_coffees WHERE origin = coffee_history.origin))::numeric(10,2) AS cost FROM coffee_history WHERE id IN (SELECT id FROM decaf_coffees) GROUP BY origin ORDER BY cost DESC");
141
-                        break;
142
-                }
129
+				query.prepare("SELECT DISTINCT origin, (avg(rate)/:conversion)::numeric(10,2) AS rate, (SELECT avg(cost)*:conversion2 FROM purchase WHERE item IN (SELECT id FROM decaf_coffees WHERE origin = coffee_history.origin))::numeric(10,2) AS cost, (SELECT avg(cost)*:conversion3 FROM purchase WHERE item IN (SELECT id FROM decaf_coffees WHERE origin = coffee_history.origin) AND time = (SELECT max(time) FROM purchase WHERE item IN (SELECT id FROM decaf_coffees WHERE origin = coffee_history.origin))), (SELECT max(time)::date FROM purchase WHERE item IN (SELECT id FROM decaf_coffees WHERE origin = coffee_history.origin)) FROM coffee_history WHERE id IN (SELECT id FROM decaf_coffees) GROUP BY origin ORDER BY " + orderClause);
143 130
 				query.bind(":conversion", conversion);
144 131
 				query.bind(":conversion2", conversion);
132
+				query.bind(":conversion3", conversion);
145 133
 				query.exec();
146 134
                 while(query.next())
147 135
                 {
@@ -149,8 +137,11 @@
149 137
                     output.writeTextElement("td", query.value(0));
150 138
                     output.writeTextElement("td", query.value(1));
151 139
                     output.writeTextElement("td", query.value(2));
140
+					output.writeTextElement("td", query.value(3));
141
+					output.writeTextElement("td", query.value(4));
152 142
                     output.writeEndElement();
153 143
                 }
144
+				query = query.invalidate();
154 145
                 output.writeEndElement();
155 146
                 output.writeEndElement();
156 147
                 output.writeEndElement();

+ 1
- 1
config/Reports/chart.xml View File

@@ -144,7 +144,6 @@
144 144
                 output.writeAttribute("y2", "450");
145 145
                 output.writeAttribute("style", "stroke:rgb(0,0,0);stroke-width:1;");
146 146
                 output.writeEndElement();
147
-                var query = new QSqlQuery();
148 147
                 var q = "SELECT "+curEndDate+"::date - "+curStartDate+"::date";
149 148
                 query.exec(q);
150 149
                 query.next();
@@ -425,6 +424,7 @@
425 424
                         i = days - 1;
426 425
                     }
427 426
                 }
427
+				query = query.invalidate();
428 428
                 output.writeStartElement("rect");
429 429
                 output.writeAttribute("fill", "rgb(0,0,0)");
430 430
                 output.writeAttribute("x", "45");

+ 49
- 31
config/Reports/cogr.xml View File

@@ -46,7 +46,8 @@
46 46
 						recipes.push(query.value(0));
47 47
 						recipeQuantities.push(query.value(1));
48 48
 					} else {
49
-						print("Error 1");
49
+						recipes.push("{-1}");
50
+						recipeQuantities.push("{-1}");
50 51
 					}
51 52
 				}
52 53
 				query.prepare("SELECT min(unroasted_total_quantity / roasted_quantity), max(unroasted_total_quantity / roasted_quantity), avg(unroasted_total_quantity / roasted_quantity) FROM roasting_log WHERE roasted_id = :ri AND unroasted_id = :gi AND unroasted_total_quantity > 0 AND roasted_quantity > 0 AND approval = true");
@@ -54,49 +55,66 @@
54 55
 				var maxes = new Array();
55 56
 				var means = new Array();
56 57
 				for(var i = 0; i < roastedItems.length; i++) {
57
-					query.bind(":ri", Number(roastedItems[i]));
58
-					query.bind(":gi", recipes[i]);
59
-					query.exec();
60
-					if(query.next()) {
61
-						mins.push(query.value(0));
62
-						maxes.push(query.value(1));
63
-						means.push(query.value(2));
58
+					if(recipes[i] == "{-1}") {
59
+						mins.push("undefined");
60
+						maxes.push("undefined");
61
+						means.push("undefined");
64 62
 					} else {
65
-						print("Error 2");
63
+						query.bind(":ri", Number(roastedItems[i]));
64
+						query.bind(":gi", recipes[i]);
65
+						query.exec();
66
+						if(query.next()) {
67
+							mins.push(query.value(0));
68
+							maxes.push(query.value(1));
69
+							means.push(query.value(2));
70
+						} else {
71
+							print("Error 2");
72
+						}
66 73
 					}
67 74
 				}
68 75
 				var proportionalCosts = new Array();
69 76
 				query.prepare("SELECT cost * :proportion FROM purchase WHERE item = :id");
70 77
 				for(var i = 0; i < roastedItems.length; i++) {
71
-					greens = sqlToArray(recipes[i]);
72
-					weights = sqlToArray(recipeQuantities[i]);
73
-					proportions = new Array();
74
-					quantitySum = weights.reduce(function(p, c) {
75
-						return p + c;
76
-					});
77
-					for(var j = 0; j < weights.length; j++) {
78
-						proportions.push(weights[j]/quantitySum);
79
-					}
80
-					partialSum = 0;
81
-					for(var j = 0; j < greens.length; j++) {
82
-						query.bind(":proportion", proportions[j]);
83
-						query.bind(":id", greens[j]);
84
-						query.exec();
85
-						if(query.next()) {
86
-							partialSum += query.value(0);
87
-						} else {
88
-							print("Error 3");
78
+					if(recipes[i] == "{-1}") {
79
+						proportionalCosts.push("undefined");
80
+					} else {
81
+						greens = sqlToArray(recipes[i]);
82
+						weights = sqlToArray(recipeQuantities[i]);
83
+						proportions = new Array();
84
+						quantitySum = weights.reduce(function(p, c) {
85
+							return p + c;
86
+						});
87
+						for(var j = 0; j < weights.length; j++) {
88
+							proportions.push(weights[j]/quantitySum);
89 89
 						}
90
+						partialSum = 0;
91
+						for(var j = 0; j < greens.length; j++) {
92
+							query.bind(":proportion", proportions[j]);
93
+							query.bind(":id", greens[j]);
94
+							query.exec();
95
+							if(query.next()) {
96
+								partialSum += query.value(0);
97
+							} else {
98
+								print("Error 3");
99
+							}
100
+						}
101
+						proportionalCosts.push(partialSum);
90 102
 					}
91
-					proportionalCosts.push(partialSum);
92 103
 				}
104
+				query = query.invalidate();
93 105
 				var minCosts = new Array();
94 106
 				var maxCosts = new Array();
95 107
 				var meanCosts = new Array();
96 108
 				for(var i = 0; i < roastedItems.length; i++) {
97
-					minCosts.push(proportionalCosts[i] * mins[i]);
98
-					maxCosts.push(proportionalCosts[i] * maxes[i]);
99
-					meanCosts.push(proportionalCosts[i] * means[i]);
109
+					if(recipes[i] == "{-1}") {
110
+						minCosts.push("undefined");
111
+						maxCosts.push("undefined");
112
+						meanCosts.push("undefined");
113
+					} else {
114
+						minCosts.push(proportionalCosts[i] * mins[i]);
115
+						maxCosts.push(proportionalCosts[i] * maxes[i]);
116
+						meanCosts.push(proportionalCosts[i] * means[i]);
117
+					}
100 118
 				}
101 119
 				output.writeStartElement("table");
102 120
 				output.writeAttribute("rules", "groups");

+ 40
- 21
config/Reports/dailyproductiondetail.xml View File

@@ -135,10 +135,10 @@
135 135
 					output.writeEndElement();
136 136
 					var unroastedList = sqlToArray(query.value(4));
137 137
 					output.writeStartElement("td");
138
+					var greensQuery = new QSqlQuery();
139
+					greensQuery.prepare("SELECT name, reference FROM coffees WHERE id = :id");
138 140
 					for(var i = 0;  i < unroastedList.length; i++)
139
-					{
140
-						var greensQuery = new QSqlQuery();
141
-						greensQuery.prepare("SELECT name, reference FROM coffees WHERE id = :id");
141
+					{	
142 142
 						greensQuery.bind(":id", Number(unroastedList[i]));
143 143
 						greensQuery.exec();
144 144
 						if(i != 0)
@@ -148,6 +148,7 @@
148 148
 						greensQuery.next();
149 149
 						output.writeCDATA(greensQuery.value(0) + " (" + unroastedList[i] + ")" + (greensQuery.value(1) == "" ? "" : " ref: " + greensQuery.value(1)));
150 150
 					}
151
+					greensQuery = greensQuery.invalidate();
151 152
 					output.writeEndElement();
152 153
 					switch(unitBox.currentIndex)
153 154
 					{
@@ -311,73 +312,91 @@
311 312
 				output.writeEndElement();
312 313
 				output.writeEndElement();
313 314
 				output.writeStartElement("tbody");
314
-				q = "SELECT DISTINCT item, (SELECT name FROM items WHERE id = item) AS name, (SELECT out FROM coffee_history WHERE id = item) FROM all_transactions WHERE time > '" + dateString + "' AND time < '" + dateString + "'::date + integer '1' ORDER BY name ASC";
315
+				q = "SELECT DISTINCT item, (SELECT name FROM items WHERE id = item) AS name, (SELECT out FROM coffee_history WHERE id = item) FROM all_transactions WHERE time >= '" + dateString + "' AND time < '" + dateString + "'::date + integer '1' ORDER BY name ASC";
315 316
 				query.exec(q)
316 317
 				var subQuery = new QSqlQuery();
317 318
 				var qq;
318 319
 				while(query.next())
319 320
 				{
320
-					output.writeStartElement("tr");
321
-					output.writeTextElement("td", query.value(1));
322
-					qq = "SELECT balance FROM item_history(" + query.value(0) + ") WHERE time = (SELECT max(time) FROM all_transactions WHERE time < '" + dateString + "' AND item = " + query.value(0) + ") OR time = (SELECT max(time) FROM all_transactions WHERE time < '" + dateString + "'::date + integer '1' AND item = " + query.value(0) + ") ORDER BY time ASC";
321
+					var validRow = true;
322
+					var td1 = query.value(1);
323
+					var td2 = 0;
324
+					var td3 = 0;
325
+					var td4 = 0;
326
+					var td5 = query.value(2);
327
+					qq = "SELECT balance FROM item_history(" + query.value(0) + ") WHERE time = (SELECT max(time) FROM all_transactions WHERE time <= '" + dateString + "' AND item = " + query.value(0) + ") OR time = (SELECT max(time) FROM all_transactions WHERE time < '" + dateString + "'::date + integer '1' AND item = " + query.value(0) + ") ORDER BY time ASC";
323 328
 					subQuery.exec(qq);
324
-					var startValue = 0;
325
-					var endValue = 0;
329
+					var startValue = "0.0";
330
+					var endValue = "0.0";
326 331
 					if(subQuery.next())
327 332
 					{
328 333
 						switch(unitBox.currentIndex)
329 334
 						{
330 335
 							case 0:
331
-								output.writeTextElement("td", (subQuery.value(0)/2.2).toFixed(subQuery.value(0).split('.').length > 1 ? subQuery.value(0).split('.')[1].length : 0));
336
+								td2 = (subQuery.value(0)/2.2).toFixed(subQuery.value(0).split('.').length > 1 ? subQuery.value(0).split('.')[1].length : 0);
332 337
 								break;
333 338
 							case 1:
334
-								output.writeTextElement("td", subQuery.value(0));
339
+								td2 = subQuery.value(0);
335 340
 								break;
336 341
 						}
337 342
 						startValue = subQuery.value(0);
338 343
 					}
339 344
 					else
340 345
 					{
341
-						output.writeEmptyElement("td");
346
+						/* This should never happen. */
347
+						validRow = false;
342 348
 					}
343 349
 					if(subQuery.next())
344 350
 					{
345 351
 						switch(unitBox.currentIndex)
346 352
 						{
347 353
 							case 0:
348
-								output.writeTextElement("td", (subQuery.value(0)/2.2).toFixed(subQuery.value(0).split('.').length > 1 ? subQuery.value(0).split('.')[1].length : 0));
354
+								td3 = (subQuery.value(0)/2.2).toFixed(subQuery.value(0).split('.').length > 1 ? subQuery.value(0).split('.')[1].length : 0);
349 355
 								break;
350 356
 							case 1:
351
-								output.writeTextElement("td", subQuery.value(0));
357
+								td3 = subQuery.value(0);
352 358
 								break;
353 359
 						}
354 360
 						endValue = subQuery.value(0);
355 361
 					}
356 362
 					else
357 363
 					{
358
-						output.writeEmptyElement("td");
364
+						/* If only one transaction exists for the current query
365
+						   this is a purchase transaction. Until something is
366
+						   done with the coffee, it should not appear in this
367
+						   section of the report. */
368
+						endValue = startValue;
369
+						validRow = false;
359 370
 					}
360 371
 					var startPrec = startValue.split('.').length > 1 ? startValue.split('.')[1].length : 0;
361 372
 					var endPrec = endValue.split('.').length > 1 ? endValue.split('.')[1].length : 0;
362 373
 					switch(unitBox.currentIndex)
363 374
 					{
364 375
 						case 0:
365
-							output.writeTextElement("td", (Number(endValue/2.2) - Number(startValue/2.2)).toFixed(Math.max(startPrec, endPrec)));
376
+							td4 = (Number(endValue/2.2) - Number(startValue/2.2)).toFixed(Math.max(startPrec, endPrec));
366 377
 							break;
367 378
 						case 1:
368
-							output.writeTextElement("td", (Number(endValue) - Number(startValue)).toFixed(Math.max(startPrec, endPrec)));
379
+							td4 = (Number(endValue) - Number(startValue)).toFixed(Math.max(startPrec, endPrec));
369 380
 							break;
370
-					}					
371
-					output.writeTextElement("td", query.value(2));
372
-					output.writeEndElement();
381
+					}
382
+					if(validRow) {
383
+						output.writeStartElement("tr");
384
+						output.writeTextElement("td", td1);
385
+						output.writeTextElement("td", td2);
386
+						output.writeTextElement("td", td3);
387
+						output.writeTextElement("td", td4);
388
+						output.writeTextElement("td", td5);
389
+						output.writeEndElement();
390
+					}
373 391
 				}
392
+				query = query.invalidate();
393
+				subQuery = subQuery.invalidate();
374 394
 				output.writeEndElement();
375 395
 				output.writeStartElement("tfoot");
376 396
 				
377 397
 				output.writeEndElement();
378 398
 				output.writeEndElement();//End of inventory table
379 399
 				
380
-				
381 400
 				output.writeEndElement();
382 401
 				output.writeEndElement();
383 402
 				output.writeEndDocument();

+ 59
- 0
config/Reports/fypurchase.xml View File

@@ -24,6 +24,7 @@
24 24
 			query.exec("SELECT EXTRACT(YEAR FROM time) FROM purchase WHERE time = (SELECT min(time) FROM purchase)");
25 25
 			query.next();
26 26
 			startDateField.setDate(query.value(0), 1, 1);
27
+			query = query.invalidate();
27 28
 			/* Set ending year to the current year. */
28 29
 			var endDateField = findChildObject(this, 'enddate');
29 30
 			endDateField.setDate(endDateField.year(), 12, 31);
@@ -79,6 +80,7 @@
79 80
 				var sacktotal = 0;
80 81
 				var unittotal = 0;
81 82
 				var costtotal = 0;
83
+				var query = new QSqlQuery();
82 84
 				for(var i = startDateField.year(); i <= endDateField.year(); i++)
83 85
 				{
84 86
 					output.writeStartElement("tr");
@@ -89,7 +91,10 @@
89 91
 					query.exec();
90 92
 					query.next();
91 93
 					output.writeStartElement("th");
94
+					output.writeStartElement("a");
95
+					output.writeAttribute("href", "typica://script/y" + i);
92 96
 					output.writeCharacters(i);
97
+					output.writeEndElement(); //a
93 98
 					output.writeEndElement(); //th
94 99
 					output.writeTextElement("td", query.value(0));
95 100
 					output.writeTextElement("td", query.value(1));
@@ -99,6 +104,7 @@
99 104
 					costtotal += Number(query.value(2));
100 105
 					output.writeEndElement(); //tr
101 106
 				}
107
+				query = query.invalidate();
102 108
 				output.writeEndElement(); //tbody
103 109
 				output.writeStartElement("tfoot");
104 110
 				output.writeTextElement("th", "Totals:");
@@ -121,6 +127,59 @@
121 127
 			endDateField.dateChanged.connect(function() {
122 128
 				refresh();
123 129
 			});
130
+			/* Expand year data */
131
+			var expandedYears = new Array();
132
+			var expandYear = function(url) {
133
+				if(expandedYears.indexOf(url) == -1) {
134
+					expandedYears.push(url);
135
+					var element = new WebElement(view.findFirstElement("#" + url));
136
+					var year = url.slice(1,url.length);
137
+					var details = '<tr><td /><td colspan="3"><table><tr><th>Id</th><th>Invoice</th><th>Vendor</th><th>Cost</th></tr>';
138
+					q = "SELECT id, invoice, vendor, (SELECT sum(cost) FROM invoice_items WHERE invoice_id = id) FROM invoices WHERE time >= '" + year + "-01-01' AND time <= '" + year + "-12-31'";
139
+					var query = new QSqlQuery();
140
+					query.exec(q);
141
+					while(query.next())
142
+					{
143
+						details += '<tr><td><a href="typica://script/i' + query.value(0) + '">' + query.value(0) + '</a></td>';
144
+						details += '<td>' + query.value(1) + '</td>';
145
+						details += '<td>' + query.value(2) + '</td>';
146
+						details += '<td>' + Number(query.value(3)).toFixed(2) + '</td></tr>';
147
+					}
148
+					query = query.invalidate();
149
+					details += '</table></td></tr>'
150
+					element.appendOutside(details);
151
+				}
152
+			};
153
+			/* Open invoices */
154
+			var openInvoice = function(url) {
155
+				var arg = url.slice(1, url.length);
156
+				var info = createWindow("invoiceinfo");
157
+				info.setInvoiceID(arg);
158
+				var query = new QSqlQuery();
159
+				query.exec("SELECT time, invoice, vendor FROM invoices WHERE id = " + arg);
160
+				query.next();
161
+				var timefield = findChildObject(info, 'date');
162
+				timefield.text = query.value(0);
163
+				var vendorfield = findChildObject(info, 'vendor');
164
+				vendorfield.text = query.value(2);
165
+				var invoicefield = findChildObject(info, 'invoice');
166
+				invoicefield.text = query.value(1);
167
+				var itemtable = findChildObject(info, 'itemtable');
168
+				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");
169
+				query = query.invalidate();
170
+			};
171
+			/* Dispatch script link clicks */
172
+			view.scriptLinkClicked.connect(function(url) {
173
+				var linkType = url[0];
174
+				switch(linkType) {
175
+					case 'y':
176
+						expandYear(url);
177
+						break
178
+					case 'i':
179
+						openInvoice(url);
180
+						break;
181
+				}
182
+			});
124 183
 		]]>
125 184
 	</program>
126 185
 </window>

+ 204
- 0
config/Reports/invchange.xml View File

@@ -0,0 +1,204 @@
1
+<window id="invchange">
2
+	<reporttitle>Inventory:->Inventory Change Summary</reporttitle>
3
+	<layout type="vertical">
4
+		<layout type="horizontal">
5
+			<label>Start Date:</label>
6
+			<calendar id="startdate" />
7
+			<label>End Date:</label>
8
+			<calendar id="enddate" />
9
+			<label>Weight Unit:</label>
10
+			<sqldrop id="unit" />
11
+			<stretch />
12
+		</layout>
13
+		<webview id="report" />
14
+	</layout>
15
+	<menu name="File">
16
+		<item id="print" shortcut="Ctrl+P">Print</item>
17
+	</menu>
18
+	<program>
19
+		<![CDATA[
20
+			this.windowTitle = "Typica - Inventory Change Summary";
21
+			var startDateField = findChildObject(this, 'startdate');
22
+			startDateField.setDate(startDateField.year(), 1, 1);
23
+			var endDateField = findChildObject(this, 'enddate');
24
+			var unitBox = findChildObject(this, 'unit');
25
+			unitBox.addItem("Kg");
26
+			unitBox.addItem("Lb");
27
+			unitBox.currentIndex = QSettings.value("script/report_unit", 1);
28
+			unitBox['currentIndexChanged(int)'].connect(function() {
29
+				QSettings.setValue("script/report_unit", unitBox.currentIndex);
30
+				refresh();
31
+			});
32
+			var view = findChildObject(this, 'report');
33
+			var printMenu = findChildObject(this, 'print');
34
+			printMenu.triggered.connect(function() {
35
+				view.print();
36
+			});
37
+			function refresh() {
38
+				var buffer = new QBuffer;
39
+				buffer.open(3);
40
+				var output = new XmlWriter(buffer);
41
+				output.writeStartDocument("1.0");
42
+				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">');
43
+				output.writeStartElement("html");
44
+				output.writeAttribute("xmlns", "http://www.w3.org/1999/xhtml");
45
+				output.writeStartElement("head");
46
+				output.writeTextElement("title", "Inventory Change Summary");
47
+				output.writeEndElement();
48
+				output.writeStartElement("body");
49
+				var startDate = "" + startDateField.year() + "-" + startDateField.month() + "-" + startDateField.day();
50
+				var endDate = "" + endDateField.year() + "-" + endDateField.month() + "-" + endDateField.day();
51
+				output.writeTextElement("h1", "Inventory Change Summary: " + startDate + " – " + endDate);
52
+				var conversion = 1;
53
+				if(unitBox.currentIndex == 0) {
54
+					conversion = 2.2;
55
+				}
56
+				var unitText = "Lb";
57
+				if(unitBox.currentIndex == 0) {
58
+					unitText = "Kg";
59
+				}
60
+				var query = new QSqlQuery();
61
+					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)*:c6 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)/:c7 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 FROM q ORDER BY name";
62
+				query.prepare(q);
63
+				query.bind(":sd1", startDate);
64
+				query.bind(":sd2", startDate);
65
+				query.bind(":sd3", startDate);
66
+				query.bind(":sd4", startDate);
67
+				query.bind(":sd6", startDate);
68
+				query.bind(":sd7", startDate);
69
+				query.bind(":ed1", endDate);
70
+				query.bind(":ed2", endDate);
71
+				query.bind(":ed3", endDate);
72
+				query.bind(":ed4", endDate);
73
+				query.bind(":ed5", endDate);
74
+				query.bind(":ed6", endDate);
75
+				query.bind(":ed7", endDate);
76
+				query.bind(":c1", conversion);
77
+				query.bind(":c2", conversion);
78
+				query.bind(":c3", conversion);
79
+				query.bind(":c4", conversion);
80
+				query.bind(":c5", conversion);
81
+				query.bind(":c6", conversion);
82
+				query.bind(":c7", conversion);
83
+				query.bind(":c8", conversion);
84
+				query.bind(":c9", conversion);
85
+				query.bind(":c10", conversion);
86
+				query.bind(":c11", conversion);
87
+				query.bind(":c12", conversion);
88
+				query.bind(":c13", conversion);
89
+				query.exec();
90
+				output.writeStartElement("table");
91
+				output.writeAttribute("rules", "groups");
92
+				output.writeAttribute("cellpadding", "3px");
93
+				output.writeStartElement("thead");
94
+				output.writeStartElement("tr");
95
+				output.writeTextElement("th", "ID"); // 0
96
+				output.writeTextElement("th", "Coffee"); // 1
97
+				output.writeTextElement("th", "Reference"); // 2
98
+				output.writeTextElement("th", "Starting (" + unitText + ")"); // 3
99
+				output.writeTextElement("th", "Cost"); // 10
100
+				output.writeTextElement("th", "Purchase (" + unitText + ")"); // 4
101
+				output.writeTextElement("th", "Cost"); // 11
102
+				output.writeTextElement("th", "Use (" + unitText + ")"); // 5
103
+				output.writeTextElement("th", "Cost"); // 12
104
+				output.writeTextElement("th", "Sale (" + unitText + ")"); // 6
105
+				output.writeTextElement("th", "Cost"); // 13
106
+				output.writeTextElement("th", "Adjustment (" + unitText + ")"); // 9
107
+				output.writeTextElement("th", "Cost"); // 15
108
+				output.writeTextElement("th", "Ending (" + unitText + ")"); // 7
109
+				output.writeTextElement("th", "Cost"); // 14
110
+				output.writeEndElement();
111
+				output.writeEndElement();
112
+				output.writeStartElement("tbody");
113
+				var sum3 = 0;
114
+				var sum10 = 0;
115
+				var sum4 = 0;
116
+				var sum11 = 0;
117
+				var sum5 = 0;
118
+				var sum12 = 0;
119
+				var sum6 = 0;
120
+				var sum13 = 0;
121
+				var sum9 = 0;
122
+				var sum15 = 0;
123
+				var sum7 = 0;
124
+				var sum14 = 0;;
125
+				while(query.next())
126
+				{
127
+					output.writeStartElement("tr");
128
+					output.writeStartElement("td");
129
+					output.writeStartElement("a");
130
+					output.writeAttribute("href", "typica://script/" + query.value(0));
131
+					output.writeTextElement("span", query.value(0)); //ID
132
+					output.writeEndElement();
133
+					output.writeEndElement();
134
+					output.writeTextElement("td", query.value(1)); //Coffee
135
+					output.writeTextElement("td", query.value(2)); //Reference
136
+					output.writeTextElement("td", parseFloat(query.value(3)).toFixed(2)); //Starting Wt
137
+					output.writeTextElement("td", parseFloat(query.value(10)).toFixed(2)); //Starting Cost
138
+					output.writeTextElement("td", parseFloat(query.value(4)).toFixed(2)); //Purchase Wt
139
+					output.writeTextElement("td", parseFloat(query.value(11)).toFixed(2)); //Purchase Cost
140
+					output.writeTextElement("td", parseFloat(query.value(5)).toFixed(2)); //Use Wt
141
+					output.writeTextElement("td", parseFloat(query.value(12)).toFixed(2)); //Use Cost
142
+					output.writeTextElement("td", parseFloat(query.value(6)).toFixed(2)); //Sale Wt
143
+					output.writeTextElement("td", parseFloat(query.value(13)).toFixed(2)); //Sale Cost
144
+					output.writeTextElement("td", parseFloat(query.value(9)).toFixed(2)); //Adjustment Wt
145
+					output.writeTextElement("td", parseFloat(query.value(15)).toFixed(2)); //Adjustment Cost
146
+					output.writeTextElement("td", parseFloat(query.value(7)).toFixed(2)); //Ending Wt
147
+					output.writeTextElement("td", parseFloat(query.value(14)).toFixed(2)); //Ending Cost
148
+					output.writeEndElement();
149
+					sum3 += parseFloat(query.value(3));
150
+					sum10 += parseFloat(query.value(10));
151
+					sum4 += parseFloat(query.value(4));
152
+					sum11 += parseFloat(query.value(11));
153
+					sum5 += parseFloat(query.value(5));
154
+					sum12 += parseFloat(query.value(12));
155
+					sum6 += parseFloat(query.value(6));
156
+					sum13 += parseFloat(query.value(13));
157
+					sum9 += parseFloat(query.value(9));
158
+					sum15 += parseFloat(query.value(15));
159
+					sum7 += parseFloat(query.value(7));
160
+					sum14 += parseFloat(query.value(14));
161
+				}
162
+				output.writeEndElement(); // tbody
163
+				output.writeStartElement("tfoot");
164
+				output.writeStartElement("tr");
165
+				output.writeTextElement("td", "");
166
+				output.writeTextElement("td", "");
167
+				output.writeTextElement("th", "Total:");
168
+				output.writeTextElement("td", sum3.toFixed(2));
169
+				output.writeTextElement("td", sum10.toFixed(2));
170
+				output.writeTextElement("td", sum4.toFixed(2));
171
+				output.writeTextElement("td", sum11.toFixed(2));
172
+				output.writeTextElement("td", sum5.toFixed(2));
173
+				output.writeTextElement("td", sum12.toFixed(2));
174
+				output.writeTextElement("td", sum6.toFixed(2));
175
+				output.writeTextElement("td", sum13.toFixed(2));
176
+				output.writeTextElement("td", sum9.toFixed(2));
177
+				output.writeTextElement("td", sum15.toFixed(2));
178
+				output.writeTextElement("td", sum7.toFixed(2));
179
+				output.writeTextElement("td", sum14.toFixed(2));
180
+				output.writeEndElement(); // tr
181
+				output.writeEndElement(); // tfoot
182
+				output.writeEndElement();
183
+				output.writeEndElement();
184
+				output.writeEndElement();
185
+				output.writeEndDocument();
186
+				view.setContent(buffer);
187
+				buffer.close();
188
+				query = query.invalidate();
189
+			}
190
+			refresh();
191
+			startDateField.dateChanged.connect(function() {
192
+				refresh();
193
+			});
194
+			endDateField.dateChanged.connect(function() {
195
+				refresh();
196
+			});
197
+			view.scriptLinkClicked.connect(function(url) {
198
+				var itemReport = createReport("itemtransactions.xml");
199
+				var sIB = findChildObject(itemReport, 'item');
200
+				sIB.currentIndex = sIB.findData(url);
201
+			});
202
+		]]>
203
+	</program>
204
+</window>

+ 2
- 1
config/Reports/inventory.xml View File

@@ -174,7 +174,8 @@
174 174
                 output.writeTextElement("td", query.value(1));
175 175
                 output.writeTextElement("td", "");
176 176
                 output.writeTextElement("td", "");
177
-                output.writeEndElement();
177
+                query = query.invalidate();
178
+				output.writeEndElement();
178 179
                 output.writeEndElement();
179 180
                 output.writeEndElement();
180 181
                 output.writeEndElement();

+ 199
- 0
config/Reports/itemtransactions.xml View File

@@ -0,0 +1,199 @@
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 = "Typica - Item Transactions";
22
+			var itemBox = findChildObject(this, 'item');
23
+			var unitBox = findChildObject(this, 'unit');
24
+			unitBox.addItem("Kg");
25
+			unitBox.addItem("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", "Item Transactions");
49
+				output.writeStartElement("style");
50
+				output.writeAttribute("type", "text/css");
51
+				output.writeCDATA("tr.PURCHASE {background-color: #77FF77}");
52
+				output.writeCDATA("tr.USE {background-color: #FFFFFF}");
53
+				output.writeCDATA("tr.INVENTORY {background-color: #7777FF}");
54
+				output.writeCDATA("tr.SALE {background-color: #FF77FF}");
55
+				output.writeCDATA("tr.LOSS {background-color: #FF7777}");
56
+				output.writeCDATA("tr.MAKE {background-color: #FFFF77}");
57
+				output.writeEndElement(); // style
58
+				
59
+				output.writeEndElement();
60
+				output.writeStartElement("body");
61
+				output.writeTextElement("h1", "Item Transactions:");
62
+				output.writeStartElement("table");
63
+				output.writeStartElement("tr");
64
+				output.writeStartElement("td");
65
+				output.writeTextElement("strong", "Item: ")
66
+				output.writeTextElement("span", itemBox.currentText);
67
+				output.writeEndElement(); // td
68
+				var query = new QSqlQuery();
69
+				query.prepare("SELECT reference, category FROM items WHERE id = :item");
70
+				query.bind(":item", itemBox.currentData());
71
+				query.exec();
72
+				if(query.next()) {
73
+					output.writeStartElement("td");
74
+					output.writeTextElement("strong", "Reference: ");
75
+					output.writeTextElement("span", query.value(0));
76
+					output.writeEndElement(); // td
77
+					output.writeStartElement("td");
78
+					output.writeTextElement("strong", "Category: ");
79
+					output.writeTextElement("span", query.value(1));
80
+					output.writeEndElement(); //td
81
+					output.writeEndElement(); //tr
82
+					query.prepare("SELECT origin, region, producer, grade, milling, drying FROM coffees WHERE id = :item");
83
+					query.bind(":item", itemBox.currentData());
84
+					query.exec();
85
+					if(query.next()) {
86
+						output.writeStartElement("tr");
87
+						output.writeStartElement("td");
88
+						output.writeTextElement("strong", "Origin: ");
89
+						output.writeTextElement("span", query.value(0));
90
+						output.writeEndElement(); // td
91
+						output.writeStartElement("td");
92
+						output.writeTextElement("strong", "Region: ");
93
+						output.writeTextElement("span", query.value(1));
94
+						output.writeEndElement(); // td
95
+						output.writeStartElement("td");
96
+						output.writeTextElement("strong", "Producer: ");
97
+						output.writeTextElement("span", query.value(2));
98
+						output.writeEndElement(); // td
99
+						output.writeEndElement(); // tr
100
+						output.writeStartElement("tr");
101
+						output.writeStartElement("td");
102
+						output.writeTextElement("strong", "Grade: ");
103
+						output.writeTextElement("span", query.value(3));
104
+						output.writeEndElement(); // td
105
+						output.writeStartElement("td");
106
+						output.writeTextElement("strong", "Milling: ");
107
+						output.writeTextElement("span", query.value(4));
108
+						output.writeEndElement(); // td
109
+						output.writeStartElement("td");
110
+						output.writeTextElement("strong", "Drying: ");
111
+						output.writeTextElement("span", query.value(5));
112
+						output.writeEndElement(); // td
113
+						output.writeEndElement(); // tr
114
+						query.prepare("SELECT decaf_method FROM decaf_coffees WHERE id = :item");
115
+						query.bind(":item", itemBox.currentData());
116
+						query.exec();
117
+						if(query.next()) {
118
+							output.writeStartElement("tr");
119
+							output.writeStartElement("td");
120
+							output.writeAttribute("colspan", "3");
121
+							output.writeTextElement("strong", "Decaffeination Method: ");
122
+							output.writeTextElement("span", query.value(0));
123
+							output.writeEndElement(); // td
124
+							output.writeEndElement(); // tr
125
+						}
126
+					}
127
+					output.writeEndElement() // table
128
+					
129
+					query.prepare("SELECT time::date, type, quantity / :c1, balance / :c2 FROM item_history(:item)");
130
+					switch(unitBox.currentIndex)
131
+					{
132
+						case 0:
133
+							query.bind(":c1", 2.2);
134
+							query.bind(":c2", 2.2);
135
+							break;
136
+						case 1:
137
+							query.bind(":c1", 1);
138
+							query.bind(":c2", 1);
139
+							break;
140
+					}
141
+					query.bind(":item", itemBox.currentData());
142
+					query.exec();
143
+					output.writeStartElement("table");
144
+					output.writeStartElement("tr");
145
+					output.writeTextElement("th", "Date");
146
+					output.writeTextElement("th", "Type");
147
+					output.writeTextElement("th", "Quantity");
148
+					output.writeTextElement("th", "Balance");
149
+					output.writeEndElement(); // tr
150
+					var prev_balance = "0";
151
+					var prev_prec = 0;
152
+					var cur_prec = 0;
153
+					while(query.next()) {
154
+						output.writeStartElement("tr");
155
+						output.writeAttribute("class", query.value(1));
156
+						output.writeTextElement("td", query.value(0));
157
+						output.writeTextElement("td", query.value(1));
158
+						if(query.value(1) == "INVENTORY") {
159
+							var split = prev_balance.split('.');
160
+							if(split.length > 1) {
161
+								prev_prec = split[1].length;
162
+							} else {
163
+								prev_prec = 0;
164
+							}
165
+							split = query.value(2).split('.');
166
+							if(split.length > 1) {
167
+								cur_prec = split[1].length;
168
+							} else {
169
+								cur_prec = 0;
170
+							}
171
+							var prec = prev_prec > cur_prec ? prev_prec : cur_prec;
172
+							output.writeTextElement("td", (Number(query.value(2)) - Number(prev_balance)).toFixed(prec));
173
+						} else {
174
+							output.writeTextElement("td", query.value(2));
175
+						}
176
+						output.writeTextElement("td", query.value(3));
177
+						prev_balance = query.value(3);
178
+						output.writeEndElement(); // tr
179
+					}
180
+					output.writeEndElement(); // table
181
+					/* Put the rest of the report here. No sense running queries if
182
+					   the item doesn't exist. */
183
+				} else {
184
+					/* Close tags if item data not found. */
185
+					output.writeEndElement(); // tr
186
+					output.writeEndElement(); // table
187
+				}
188
+				
189
+				output.writeEndElement(); // body
190
+				output.writeEndElement(); // html
191
+				output.writeEndDocument();
192
+				view.setContent(buffer);
193
+				buffer.close();
194
+				query = query.invalidate();
195
+			}
196
+			refresh();
197
+		]]>
198
+	</program>
199
+</window>

+ 2
- 1
config/Reports/rwacp.xml View File

@@ -114,7 +114,8 @@
114 114
 				query.exec();
115 115
 				query.next();
116 116
                 output.writeTextElement("td", query.value(0));
117
-                output.writeEndElement();
117
+                query = query.invalidate();
118
+				output.writeEndElement();
118 119
                 output.writeEndElement();
119 120
                 output.writeEndElement();
120 121
                 output.writeEndElement();

+ 1
- 0
config/Windows/batchdetailsnew.xml View File

@@ -295,6 +295,7 @@
295 295
 				rq = rq + 'string($t/annotation), "~")';
296 296
 				colQuery.setQuery(rq);
297 297
 				var annotationData = colQuery.exec();
298
+				colQuery = colQuery.invalidate();
298 299
 				buffer2.close();
299 300
 				output.writeStartElement("tbody");
300 301
 				var annotationRecords = annotationData.split("~");

+ 35
- 3
config/Windows/navigation.xml View File

@@ -210,8 +210,6 @@ type="push" />
210 210
 			query.exec("CREATE VIEW short_log AS SELECT roasting_log.\"time\", (SELECT items.name FROM items WHERE (items.id = roasting_log.roasted_id)) AS name, roasting_log.unroasted_total_quantity, roasting_log.roasted_quantity, ((((roasting_log.unroasted_total_quantity - roasting_log.roasted_quantity) / roasting_log.unroasted_total_quantity) * (100)::numeric))::numeric(12,2) AS weight_loss, roasting_log.duration FROM roasting_log ORDER BY roasting_log.\"time\"");
211 211
 			query.exec("CREATE FUNCTION add_inventory() RETURNS trigger AS $$ BEGIN UPDATE items SET quantity = quantity + NEW.quantity WHERE id = NEW.item; RETURN NEW; END; $$ LANGUAGE plpgsql");
212 212
 			query.exec("CREATE FUNCTION bags_in_stock(bigint) RETURNS numeric AS $_$SELECT quantity / (SELECT conversion FROM lb_bag_conversion WHERE item = id) FROM items WHERE id = $1;$_$ LANGUAGE sql IMMUTABLE STRICT");
213
-			query.exec("CREATE FUNCTION calculate_inventory_balance() RETURNS trigger AS $$ DECLARE old_quantity numeric; BEGIN old_quantity := (SELECT balance FROM working WHERE time = (SELECT max(time) FROM working)); IF old_quantity IS NULL THEN old_quantity := 0; END IF; IF NEW.type = 'PURCHASE' OR NEW.type = 'MAKE' THEN NEW.balance := old_quantity + NEW.quantity; ELSE IF NEW.type = 'INVENTORY' THEN NEW.balance := NEW.quantity; ELSE IF NEW.type = 'USE' OR NEW.type = 'SALE' OR NEW.type = 'LOSS' THEN NEW.balance := old_quantity - NEW.quantity; END IF; END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql");
214
-			query.exec("CREATE FUNCTION item_history(bigint) RETURNS SETOF item_transaction_with_balance AS $_$ DECLARE r item_transaction_with_balance; BEGIN CREATE TEMPORARY TABLE working(time timestamp without time zone, item bigint, quantity numeric, cost numeric, vendor text, reason text, customer text, type text, balance numeric) ON COMMIT DROP; CREATE TRIGGER quantity_update BEFORE INSERT ON working FOR EACH ROW EXECUTE PROCEDURE calculate_inventory_balance(); INSERT INTO working SELECT time, item, quantity, cost, vendor, reason, customer, type, NULL AS balance FROM all_transactions WHERE item = $1 ORDER BY time ASC; FOR r IN SELECT time, item, quantity, cost, vendor, reason, customer, type, balance FROM working LOOP RETURN NEXT r; END LOOP; DROP TABLE working; RETURN; END; $_$ LANGUAGE plpgsql");
215 213
 			query.exec("CREATE FUNCTION log_make() RETURNS trigger AS $$ BEGIN IF NEW.roasted_quantity IS NOT NULL THEN INSERT INTO make VALUES(NEW.time, NEW.roasted_id, NEW.roasted_quantity); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql");
216 214
 			query.exec("CREATE 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 VALUES(NEW.time, NEW.roasted_id, NEW.roasted_quantity); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql");
217 215
 			query.exec("CREATE FUNCTION log_use() RETURNS trigger AS $$ DECLARE i integer := array_lower(NEW.unroasted_id, 1); u integer := array_upper(NEW.unroasted_id, 1); BEGIN WHILE i <= u LOOP INSERT INTO use VALUES(NEW.time, NEW.unroasted_id[i], NEW.unroasted_quantity[i]); i := i + 1; END LOOP; RETURN NEW; END; $$ LANGUAGE plpgsql");
@@ -252,7 +250,36 @@ type="push" />
252 250
 			query.exec("INSERT INTO TypicaFeatures VALUES('sample-roasting', TRUE, 1)");
253 251
 			query = query.invalidate();
254 252
 		};
255
-
253
+		
254
+		/* Some changes to the database are required to log who performed what tasks.
255
+		   This adds logging for all transaction types and also for the roasting log.*/
256
+		var DBUpdateMultiUser = function() {
257
+			var query = new QSqlQuery;
258
+			query.exec("ALTER TABLE transactions ADD COLUMN person text DEFAULT NULL");
259
+			query.exec("ALTER TABLE roasting_log ADD COLUMN person text DEFAULT NULL");
260
+			query.exec("CREATE OR REPLACE FUNCTION log_session_user() RETURNS trigger AS $$ BEGIN NEW.person := session_user; RETURN NEW; END; $$ LANGUAGE plpgsql");
261
+			query.exec("CREATE TRIGGER log_person BEFORE INSERT ON inventory FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
262
+			query.exec("CREATE TRIGGER log_person BEFORE INSERT ON loss FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
263
+			query.exec("CREATE TRIGGER log_person BEFORE INSERT ON make FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
264
+			query.exec("CREATE TRIGGER log_person BEFORE INSERT ON purchase FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
265
+			query.exec("CREATE TRIGGER log_person BEFORE INSERT ON sale FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
266
+			query.exec("CREATE TRIGGER log_person BEFORE INSERT ON use FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
267
+			query.exec("CREATE TRIGGER log_person BEFORE INSERT ON roasting_log FOR EACH ROW EXECUTE PROCEDURE log_session_user()");
268
+			query.exec("UPDATE TypicaFeatures SET version = 2 WHERE feature = 'base-features'");
269
+			query = query.invalidate();
270
+		};
271
+		
272
+		/* Bug fix and optimization for item_history */
273
+		var DBUpdateHistory = function() {
274
+			var query = new QSqlQuery;
275
+			query.exec("CREATE TYPE transaction_type AS (type text, quantity numeric)");
276
+			query.exec("CREATE FUNCTION update_balance(numeric, transaction_type) RETURNS numeric AS $$ BEGIN CASE $2.type WHEN 'PURCHASE', 'MAKE' THEN RETURN $1 + $2.quantity; WHEN 'INVENTORY' THEN RETURN $2.quantity; WHEN 'USE', 'SALE', 'LOSS' THEN RETURN $1 - $2.quantity; END CASE; END; $$ LANGUAGE plpgsql STRICT");
277
+			query.exec("CREATE AGGREGATE transaction_balance (BASETYPE = transaction_type, SFUNC = update_balance, STYPE = numeric, INITCOND = '0')");
278
+			query.exec("CREATE OR REPLACE FUNCTION item_history(bigint) RETURNS SETOF item_transaction_with_balance AS $$ SELECT time, item, quantity, cost, vendor, reason, customer, type, transaction_balance((type, quantity)::transaction_type) OVER (PARTITION BY item ORDER BY time ASC) AS balance FROM all_transactions WHERE item = $1; $$ LANGUAGE SQL");
279
+			query.exec("DROP FUNCTION calculate_inventory_balance()");
280
+			query = query.invalidate();
281
+		};
282
+		
256 283
 		query = new QSqlQuery();
257 284
 		/* A table keeps track of database versioning information. This table is created
258 285
 		   if required. */
@@ -267,6 +294,11 @@ type="push" />
267 294
 			{
268 295
 				DBCreateBase();
269 296
 			}
297
+			if(query.value(2) < 2)
298
+			{
299
+				DBUpdateMultiUser();
300
+				DBUpdateHistory();
301
+			}
270 302
 		}
271 303
 		else
272 304
 		{

+ 16
- 1
config/Windows/newbatch.xml View File

@@ -21,7 +21,7 @@
21 21
         </layout>
22 22
         <label>Green Coffee:</label>
23 23
         <sqltablearray columns="2" id="greens">
24
-            <column name="Coffee" delegate="sql" showdata="true" null="false" data="0" display="1">SELECT id, name FROM coffees WHERE quantity &lt;&gt; 0 ORDER BY name</column>
24
+            <column name="Coffee" delegate="sql" showdata="true" null="true" nulltext="Delete" nulldata="delete" data="0" display="1">SELECT id, name FROM coffees WHERE quantity &lt;&gt; 0 ORDER BY name</column>
25 25
             <column name="Weight" />
26 26
         </sqltablearray>
27 27
         <layout type="horizontal">
@@ -119,6 +119,20 @@
119 119
 				}
120 120
 			}
121 121
 			model.dataChanged.connect(function() {
122
+				var deleteRow = -1;
123
+				/* The combo box delegate updates user data before display data
124
+				   and this code is executed before the model update is fully
125
+				   complete. Rather than rely on this behavior continuing, we
126
+				   check that the display value has also been updated and defer
127
+				   row removal until both updates are complete.
128
+				*/
129
+				while((deleteRow = table.findData("delete", 0)) > -1) {
130
+					if(table.data(deleteRow, 0, 0) == "Delete") {
131
+						table.removeRow(table.findData("delete", 0));
132
+					} else {
133
+						break;
134
+					}
135
+				}
122 136
 				green.text = table.columnSum(1, 0);
123 137
 				table.resizeColumnToContents(0);
124 138
                 if(parseFloat(green.text) > 0)
@@ -160,6 +174,7 @@
160 174
             var profilebutton = findChildObject(this, 'load');
161 175
             profilebutton.setEnabled(false);
162 176
             roasted['currentIndexChanged(int)'].connect(function() {
177
+				table.clear();
163 178
                 var query = new QSqlQuery();
164 179
                 var q = "SELECT EXISTS(SELECT 1 FROM item_files WHERE item = ";
165 180
                 q = q + roasted.currentData();

+ 255
- 46
src/typica.w View File

@@ -2256,6 +2256,7 @@ QScriptValue constructXQuery(QScriptContext *context, QScriptEngine *engine);
2256 2256
 QScriptValue XQuery_bind(QScriptContext *context, QScriptEngine *engine);
2257 2257
 QScriptValue XQuery_exec(QScriptContext *context, QScriptEngine *engine);
2258 2258
 QScriptValue XQuery_setQuery(QScriptContext *context, QScriptEngine *engine);
2259
+QScriptValue XQuery_invalidate(QScriptContext *context, QScriptEngine *engine);
2259 2260
 void setXQueryProperties(QScriptValue value, QScriptEngine *engine);
2260 2261
 
2261 2262
 @ The constructor must be registered with the host environment. This is done a
@@ -2266,7 +2267,7 @@ constructor = engine->newFunction(constructXQuery);
2266 2267
 engine->globalObject().setProperty("XQuery", constructor);
2267 2268
 
2268 2269
 @ The constructor just needs to make sure the functions we want to make
2269
-available are applied.
2270
+available are applied. A method is also provided to free the |QXmlQuery|.
2270 2271
 
2271 2272
 @<Functions for scripting@>=
2272 2273
 QScriptValue constructXQuery(QScriptContext *, QScriptEngine *engine)
@@ -2276,11 +2277,19 @@ QScriptValue constructXQuery(QScriptContext *, QScriptEngine *engine)
2276 2277
 	return object;
2277 2278
 }
2278 2279
 
2280
+QScriptValue XQuery_invalidate(QScriptContext *context, QScriptEngine *)
2281
+{
2282
+	QXmlQuery *self = getself<QXmlQuery *>(context);
2283
+	delete self;
2284
+	return QScriptValue();
2285
+}
2286
+
2279 2287
 void setXQueryProperties(QScriptValue value, QScriptEngine *engine)
2280 2288
 {
2281 2289
 	value.setProperty("bind", engine->newFunction(XQuery_bind));
2282 2290
 	value.setProperty("exec", engine->newFunction(XQuery_exec));
2283 2291
 	value.setProperty("setQuery", engine->newFunction(XQuery_setQuery));
2292
+	value.setProperty("invalidate", engine->newFunction(XQuery_invalidate));
2284 2293
 }
2285 2294
 
2286 2295
 @ The |bind()| property can be used to specify a |QIODevice| to be referenced by
@@ -3369,7 +3378,7 @@ class SqlQueryConnection : public QSqlQuery@/
3369 3378
 	public:@/
3370 3379
 		SqlQueryConnection(const QString &query = QString());
3371 3380
 		~SqlQueryConnection();
3372
-		QSqlQuery* operator->();
3381
+		QSqlQuery* operator->() const;
3373 3382
 	private:@/
3374 3383
 		QString connection;
3375 3384
 		QSqlQuery *q;
@@ -3409,7 +3418,7 @@ SqlQueryConnection::~SqlQueryConnection()
3409 3418
 object.
3410 3419
 
3411 3420
 @<SqlQueryConnection implementation@>=
3412
-QSqlQuery* SqlQueryConnection::operator->()
3421
+QSqlQuery* SqlQueryConnection::operator->() const
3413 3422
 {
3414 3423
 	return q;
3415 3424
 }
@@ -3448,8 +3457,9 @@ value.
3448 3457
 @<Functions for scripting@>=
3449 3458
 QScriptValue constructQSqlQuery(QScriptContext *, QScriptEngine *engine)
3450 3459
 {
3460
+	SqlQueryConnection *obj = new SqlQueryConnection();
3451 3461
 	QScriptValue object =
3452
-	     engine->toScriptValue<void *>(new SqlQueryConnection());
3462
+	     engine->toScriptValue<void *>(obj);
3453 3463
 	setQSqlQueryProperties(object, engine);
3454 3464
 	return object;
3455 3465
 }
@@ -3478,40 +3488,40 @@ void setQSqlQueryProperties(QScriptValue value, QScriptEngine *engine)
3478 3488
 @<Functions for scripting@>=
3479 3489
 QScriptValue QSqlQuery_exec(QScriptContext *context, QScriptEngine *engine)
3480 3490
 {
3481
-	SqlQueryConnection *query = getself<SqlQueryConnection *>(context);
3491
+	QSqlQuery *q = getself<SqlQueryConnection *>(context)->operator->();
3482 3492
 	QScriptValue retval;
3483 3493
 	if(context->argumentCount() == 1)
3484 3494
 	{
3485 3495
 		retval = QScriptValue(engine,
3486
-		                      query->exec(argument<QString>(0, context)));
3496
+		                      q->exec(argument<QString>(0, context)));
3487 3497
 	}
3488 3498
 	else
3489 3499
 	{
3490
-		retval = QScriptValue(engine, query->exec());
3500
+		retval = QScriptValue(engine, q->exec());
3491 3501
 	}
3492
-	if(query->lastError().isValid())
3502
+	if(q->lastError().isValid())
3493 3503
 	{
3494
-		qDebug() << query->lastQuery();
3495
-		qDebug() << query->lastError().text();
3504
+		qDebug() << q->lastQuery();
3505
+		qDebug() << q->lastError().text();
3496 3506
 	}
3497 3507
 	return retval;
3498 3508
 }
3499 3509
 
3500 3510
 QScriptValue QSqlQuery_executedQuery(QScriptContext *context, QScriptEngine *)
3501 3511
 {
3502
-	SqlQueryConnection *query = getself<SqlQueryConnection *>(context);
3512
+	QSqlQuery *query = getself<SqlQueryConnection *>(context)->operator->();
3503 3513
 	return QScriptValue(query->lastQuery());
3504 3514
 }
3505 3515
 
3506 3516
 QScriptValue QSqlQuery_next(QScriptContext *context, QScriptEngine *engine)
3507 3517
 {
3508
-	SqlQueryConnection *query = getself<SqlQueryConnection *>(context);
3518
+	QSqlQuery *query = getself<SqlQueryConnection *>(context)->operator->();
3509 3519
 	return QScriptValue(engine, query->next());
3510 3520
 }
3511 3521
 
3512 3522
 QScriptValue QSqlQuery_value(QScriptContext *context, QScriptEngine *engine)
3513 3523
 {
3514
-	SqlQueryConnection *query = getself<SqlQueryConnection *>(context);
3524
+	QSqlQuery *query = getself<SqlQueryConnection *>(context)->operator->();
3515 3525
 	return QScriptValue(engine,
3516 3526
 	                    query->value(argument<int>(0, context)).toString());
3517 3527
 }
@@ -3522,13 +3532,13 @@ data available in a named file, or data from any open |QIODevice|.
3522 3532
 @<Functions for scripting@>=
3523 3533
 QScriptValue QSqlQuery_prepare(QScriptContext *context, QScriptEngine *engine)
3524 3534
 {
3525
-	SqlQueryConnection *query = getself<SqlQueryConnection *>(context);
3535
+	QSqlQuery *query = getself<SqlQueryConnection *>(context)->operator->();
3526 3536
 	return QScriptValue(engine, query->prepare(argument<QString>(0, context)));
3527 3537
 }
3528 3538
 
3529 3539
 QScriptValue QSqlQuery_bind(QScriptContext *context, QScriptEngine *)
3530 3540
 {
3531
-	SqlQueryConnection *query = getself<SqlQueryConnection *>(context);
3541
+	QSqlQuery *query = getself<SqlQueryConnection *>(context)->operator->();
3532 3542
 	query->bindValue(argument<QString>(0, context),
3533 3543
 	                 argument<QVariant>(1, context));
3534 3544
 	return QScriptValue();
@@ -3537,7 +3547,7 @@ QScriptValue QSqlQuery_bind(QScriptContext *context, QScriptEngine *)
3537 3547
 QScriptValue QSqlQuery_bindFileData(QScriptContext *context,
3538 3548
                                     QScriptEngine *)
3539 3549
 {
3540
-	SqlQueryConnection *query = getself<SqlQueryConnection *>(context);
3550
+	QSqlQuery *query = getself<SqlQueryConnection *>(context)->operator->();
3541 3551
 	QString placeholder = argument<QString>(0, context);
3542 3552
 	QString filename = argument<QString>(1, context);
3543 3553
 	QFile file(filename);
@@ -3554,7 +3564,7 @@ QScriptValue QSqlQuery_bindFileData(QScriptContext *context,
3554 3564
 QScriptValue QSqlQuery_bindDeviceData(QScriptContext *context,
3555 3565
                                       QScriptEngine *)
3556 3566
 {
3557
-	SqlQueryConnection *query = getself<SqlQueryConnection *>(context);
3567
+	QSqlQuery *query = getself<SqlQueryConnection *>(context)->operator->();
3558 3568
 	QString placeholder = argument<QString>(0, context);
3559 3569
 	QIODevice *device = argument<QIODevice *>(1, context);
3560 3570
 	device->reset();
@@ -3680,13 +3690,14 @@ host environment. The function is now depreciated and should not be used.
3680 3690
 @<Functions for scripting@>=
3681 3691
 QScriptValue annotationFromRecord(QScriptContext *context, QScriptEngine *)
3682 3692
 {
3683
-	SqlQueryConnection query;
3693
+	SqlQueryConnection h;
3694
+	QSqlQuery *query = h.operator->();
3684 3695
 	QString q = "SELECT file FROM files WHERE id = :file";
3685
-	query.prepare(q);
3686
-	query.bindValue(":file", argument<int>(0, context));
3687
-	query.exec();
3688
-	query.next();
3689
-	QByteArray array = query.value(0).toByteArray();
3696
+	query->prepare(q);
3697
+	query->bindValue(":file", argument<int>(0, context));
3698
+	query->exec();
3699
+	query->next();
3700
+	QByteArray array = query->value(0).toByteArray();
3690 3701
 	QBuffer buffer(&array);
3691 3702
 	buffer.open(QIODevice::ReadOnly);
3692 3703
 	QXmlQuery xquery;
@@ -3866,12 +3877,16 @@ any {\tt <program>} elements that are immediate children of the
3866 3877
 {\tt <window>} element into a window displayed on screen, the script in the
3867 3878
 {\tt <program>} elements must call a function to display a specified window.
3868 3879
 
3880
+Report windows can be produced by scripts in a similar, but slightly different
3881
+manner.
3882
+
3869 3883
 \danger This design works, but it'@q'@>s not particularly good design. It was written
3870 3884
 under severe time constraints and should be redesigned or at least cleaned up
3871 3885
 and reorganized.\endanger
3872 3886
 
3873 3887
 @<Function prototypes for scripting@>=
3874 3888
 QScriptValue createWindow(QScriptContext *context, QScriptEngine *engine);
3889
+QScriptValue createReport(QScriptContext *context, QScriptEngine *engine);
3875 3890
 void addLayoutToWidget(QDomElement element, QStack<QWidget*> *widgetStack,
3876 3891
                        QStack<QLayout*> *layoutStack);
3877 3892
 void addLayoutToLayout(QDomElement element, QStack<QWidget *> *widgetStack,
@@ -3931,12 +3946,14 @@ void addCalendarToLayout(QDomElement element, QStack<QWidget *> *widgetStack,
3931 3946
 void addSpinBoxToLayout(QDomElement element, QStack<QWidget *> *widgetStack,
3932 3947
                         QStack<QLayout *> *layoutStack);
3933 3948
 
3934
-@ The function for creating the window must be made available to the scripting
3949
+@ The functions for creating windows must be made available to the scripting
3935 3950
 engine.
3936 3951
 
3937 3952
 @<Set up the scripting engine@>=
3938 3953
 engine->globalObject().setProperty("createWindow",
3939 3954
                                    engine->newFunction(createWindow));
3955
+engine->globalObject().setProperty("createReport",
3956
+                                   engine->newFunction(createReport));
3940 3957
 
3941 3958
 @ This function must examine the configuration document in search of the
3942 3959
 appropriate window element, parse the contents of that element, and create a
@@ -3956,6 +3973,32 @@ QScriptValue createWindow(QScriptContext *context, QScriptEngine *engine)@/
3956 3973
 	return object;
3957 3974
 }
3958 3975
 
3976
+@ Report files are not part of the configuration document and must be created
3977
+differently. While there is a special menu type that handles all of this
3978
+without involving the host environment, scripted generation and manipulation of
3979
+report windows requires another function. This function will only work after a
3980
+window with a reports menu has been created.
3981
+
3982
+@<Functions for scripting@>=
3983
+QScriptValue createReport(QScriptContext *context, QScriptEngine *engine)
3984
+{
3985
+	QString targetID = argument<QString>(0, context);
3986
+	QFile file(QString("reports:%1").arg(targetID));
3987
+	QScriptValue object;
3988
+	if(file.open(QIODevice::ReadOnly))
3989
+	{
3990
+		QDomDocument document;
3991
+		document.setContent(&file, true);
3992
+		QDomElement element = document.documentElement();
3993
+		if(!element.isNull())
3994
+		{
3995
+			@<Display the window@>@;
3996
+		}
3997
+		file.close();
3998
+	}
3999
+	return object;
4000
+}
4001
+
3959 4002
 @ First we must locate the {\tt <window>} element. The most sensible way to do
3960 4003
 this would require that each {\tt <window>} element has an ID attribute and
3961 4004
 search the DOM tree for that ID. Unfortunately, as of this writing,
@@ -5103,6 +5146,14 @@ of presenting a human readable list of choices.
5103 5146
 @<Assign column delegate from SQL@>=
5104 5147
 SqlComboBoxDelegate *delegate = new SqlComboBoxDelegate;
5105 5148
 SqlComboBox *widget = new SqlComboBox();
5149
+if(currentElement.hasAttribute("nulltext"))
5150
+{
5151
+	widget->setNullText(currentElement.attribute("nulltext"));
5152
+}
5153
+if(currentElement.hasAttribute("nulldata"))
5154
+{
5155
+	widget->setNullData(QVariant(currentElement.attribute("nulldata")));
5156
+}
5106 5157
 if(currentElement.hasAttribute("null"))
5107 5158
 {
5108 5159
 	if(currentElement.attribute("null") == "true")
@@ -5556,6 +5607,9 @@ QScriptValue SaltTable_model(QScriptContext *context, QScriptEngine *engine);
5556 5607
 QScriptValue SaltTable_quotedColumnArray(QScriptContext *context,
5557 5608
                                          QScriptEngine *engine);
5558 5609
 QScriptValue SaltTable_setData(QScriptContext *context, QScriptEngine *engine);
5610
+QScriptValue SaltTable_clear(QScriptContext *context, QScriptEngine *engine);
5611
+QScriptValue SaltTable_removeRow(QScriptContext *context, QScriptEngine *engine);
5612
+QScriptValue SaltTable_findData(QScriptContext *context, QScriptEngine *engine);
5559 5613
 
5560 5614
 @ There are times when it is useful to obtain the sum of values in a column of
5561 5615
 a SaltTable object. For example, when a column represents the weight of the
@@ -5686,6 +5740,46 @@ QScriptValue SaltTable_data(QScriptContext *context, QScriptEngine *engine)
5686 5740
 	return retval;
5687 5741
 }
5688 5742
 
5743
+@ There are times when it is useful to clear the content of a table. This is
5744
+used, for example, in the green coffees table after changing the roasted coffee
5745
+item to eliminate excess rows in the case where the previously selected item
5746
+was a pre-roast blend.
5747
+
5748
+@<Functions for scripting@>=
5749
+QScriptValue SaltTable_clear(QScriptContext *context, QScriptEngine *)
5750
+{
5751
+	QTableView *self = getself<QTableView *>(context);
5752
+	SaltModel *model = qobject_cast<SaltModel *>(self->model());
5753
+	model->clear();
5754
+	return QScriptValue();
5755
+}
5756
+
5757
+@ It is sometimes useful to remove a row from a table. This is done in the new
5758
+batch window when the coffee for a row is set to a NULL item.
5759
+
5760
+@<Functions for scripting@>=
5761
+QScriptValue SaltTable_removeRow(QScriptContext *context, QScriptEngine *engine)
5762
+{
5763
+	QTableView *self = getself<QTableView *>(context);
5764
+	SaltModel *model = qobject_cast<SaltModel *>(self->model());
5765
+	int row = argument<int>(0, context);
5766
+	return engine->newVariant(model->removeRow(row));
5767
+}
5768
+
5769
+@ To remove the correct row, it is sometimes useful to query the table for
5770
+special values. This is done with the |findData()| method on the underlying
5771
+model.
5772
+
5773
+@<Functions for scripting@>=
5774
+QScriptValue SaltTable_findData(QScriptContext *context, QScriptEngine *engine)
5775
+{
5776
+	QTableView *self = getself<QTableView *>(context);
5777
+	SaltModel *model = qobject_cast<SaltModel *>(self->model());
5778
+	QVariant value = argument<QVariant>(0, context);
5779
+	int column = argument<int>(1, context);
5780
+	return engine->newVariant(model->findData(value, column));
5781
+}
5782
+
5689 5783
 @ These functions need to be added as properties of the table when it is passed
5690 5784
 to the host environment.
5691 5785
 
@@ -5705,6 +5799,9 @@ void setSaltTableProperties(QScriptValue value, QScriptEngine *engine)
5705 5799
 	value.setProperty("data", engine->newFunction(SaltTable_data));
5706 5800
 	value.setProperty("model", engine->newFunction(SaltTable_model));
5707 5801
 	value.setProperty("setData", engine->newFunction(SaltTable_setData));
5802
+	value.setProperty("clear", engine->newFunction(SaltTable_clear));
5803
+	value.setProperty("removeRow", engine->newFunction(SaltTable_removeRow));
5804
+	value.setProperty("findData", engine->newFunction(SaltTable_findData));
5708 5805
 }
5709 5806
 
5710 5807
 @ The |SqlComboBox| is another class that is not constructed from scripts but is
@@ -5719,6 +5816,7 @@ QScriptValue QComboBox_currentData(QScriptContext *context,
5719 5816
 QScriptValue QComboBox_addItem(QScriptContext *context, QScriptEngine *engine);
5720 5817
 QScriptValue QComboBox_setModel(QScriptContext *context, QScriptEngine *engine);
5721 5818
 QScriptValue QComboBox_findText(QScriptContext *context, QScriptEngine *engine);
5819
+QScriptValue QComboBox_findData(QScriptContext *context, QScriptEngine *engine);
5722 5820
 
5723 5821
 @ These functions should seem familiar by now.
5724 5822
 
@@ -5736,6 +5834,7 @@ void setQComboBoxProperties(QScriptValue value, QScriptEngine *engine)
5736 5834
 	value.setProperty("addItem", engine->newFunction(QComboBox_addItem));
5737 5835
 	value.setProperty("setModel", engine->newFunction(QComboBox_setModel));
5738 5836
 	value.setProperty("findText", engine->newFunction(QComboBox_findText));
5837
+	value.setProperty("findData", engine->newFunction(QComboBox_findData));
5739 5838
 }
5740 5839
 
5741 5840
 QScriptValue QComboBox_currentData(QScriptContext *context,
@@ -5766,6 +5865,12 @@ QScriptValue QComboBox_findText(QScriptContext *context, QScriptEngine *engine)
5766 5865
 	return QScriptValue(engine, self->findText(argument<QString>(0, context)));
5767 5866
 }
5768 5867
 
5868
+QScriptValue QComboBox_findData(QScriptContext *context, QScriptEngine *engine)
5869
+{
5870
+	QComboBox *self = getself<QComboBox *>(context);
5871
+	return QScriptValue(engine, self->findData(argument<QVariant>(0, context)));
5872
+}
5873
+
5769 5874
 @i abouttypica.w
5770 5875
 
5771 5876
 @** A representation of temperature measurements.
@@ -11802,12 +11907,13 @@ database aware widgets.
11802 11907
 QSqlDatabase Application::database()
11803 11908
 {
11804 11909
 	QString connectionName;
11805
-	QSqlDatabase connection = QSqlDatabase::database();
11910
+	QSqlDatabase connection =
11911
+		QSqlDatabase::database(QLatin1String(QSqlDatabase::defaultConnection), false);
11806 11912
 	do
11807 11913
 	{
11808 11914
 		connectionName = QUuid::createUuid().toString();
11809 11915
 	} while (QSqlDatabase::connectionNames().contains(connectionName));
11810
-	return QSqlDatabase::cloneDatabase(connection, connectionName);
11916
+	return QSqlDatabase::cloneDatabase(connection, QString(connectionName));
11811 11917
 }
11812 11918
 
11813 11919
 @** Table editor for ordered arrays with SQL relations.
@@ -11884,6 +11990,10 @@ class SaltModel : public QAbstractItemModel@/
11884 11990
 		QModelIndex parent(const QModelIndex &index) const;
11885 11991
 		QString arrayLiteral(int column, int role) const;
11886 11992
 		QString quotedArrayLiteral(int column, int role) const;
11993
+		void clear();
11994
+		bool removeRows(int row, int count,
11995
+		                const QModelIndex &parent = QModelIndex());
11996
+		int findData(const QVariant &value, int column, int role = Qt::UserRole);
11887 11997
 };
11888 11998
 
11889 11999
 @ The only unique methods in this class are the |arrayLiteral| and
@@ -12154,6 +12264,74 @@ QModelIndex SaltModel::index(int row, int column,
12154 12264
 	return QModelIndex();
12155 12265
 }
12156 12266
 
12267
+@ There are some times when it is useful to clear the model data. Note that
12268
+column header data is retained and the table will contain a single empty row
12269
+after this method is called.
12270
+
12271
+@<SaltModel Implementation@>=
12272
+void SaltModel::clear()
12273
+{
12274
+	beginResetModel();
12275
+	modelData.clear();
12276
+	@<Expand the SaltModel@>@;
12277
+	endResetModel();
12278
+}
12279
+
12280
+@ Another commonly useful operation is the ability to remove rows from the
12281
+model. The new batch window uses this feature to eliminate rows in which the
12282
+coffee is set to NULL. Note that if all rows of the model are removed, a new
12283
+empty row will be created.
12284
+
12285
+@<SaltModel Implementation@>=
12286
+bool SaltModel::removeRows(int row, int count,
12287
+                           const QModelIndex &parent)
12288
+{
12289
+	if(parent == QModelIndex())
12290
+	{
12291
+		if(row >= 0 && count > 0 && (row + count - 1) < modelData.size())
12292
+		{
12293
+			beginRemoveRows(parent, row, row + count - 1);
12294
+			for(int i = 0; i < count; i++)
12295
+			{
12296
+				modelData.removeAt(row);
12297
+			}
12298
+			endRemoveRows();
12299
+			if(modelData.size() == 0)
12300
+			{
12301
+				beginInsertRows(parent, 0, 0);
12302
+				@<Expand the SaltModel@>@;
12303
+				endInsertRows();
12304
+			}
12305
+			return true;
12306
+		}
12307
+	}
12308
+	return false;
12309
+}
12310
+
12311
+@ To find the row number for removal operations it is useful to search for
12312
+special values on a given role. The |findData()| method returns the first row
12313
+in which the given value matches for a particular column and a particular role
12314
+or |-1| if no such match exists.
12315
+
12316
+@<SaltModel Implementation@>=
12317
+int SaltModel::findData(const QVariant &value, int column, int role)
12318
+{
12319
+	for(int i = 0; i < modelData.size(); i++)
12320
+	{
12321
+		if(modelData.at(i).size() > column)
12322
+		{
12323
+			if(modelData.at(i).at(column).contains(role))
12324
+			{
12325
+				if(modelData.at(i).at(column).value(role) == value)
12326
+				{
12327
+					return i;
12328
+				}
12329
+			}
12330
+		}
12331
+	}
12332
+	return -1;
12333
+}
12334
+
12157 12335
 @* A Delegate for SQL Relations.
12158 12336
 
12159 12337
 \noindent The first column of the table view being described is responsible for
@@ -12179,6 +12357,8 @@ class SqlComboBox : public QComboBox@/
12179 12357
 	int dataColumn;
12180 12358
 	int displayColumn;
12181 12359
 	bool dataColumnShown;
12360
+	QString specialNullText;
12361
+	QVariant specialNullData;
12182 12362
 	public:@/
12183 12363
 		SqlComboBox();
12184 12364
 		~SqlComboBox();
@@ -12188,7 +12368,9 @@ class SqlComboBox : public QComboBox@/
12188 12368
 		void addSqlOptions(QString query);
12189 12369
 		void setDataColumn(int column);
12190 12370
 		void setDisplayColumn(int column);
12191
-		void showData(bool show);@t\2@>@/
12371
+		void showData(bool show);
12372
+		void setNullText(QString nullText);
12373
+		void setNullData(QVariant nullData);@t\2@>@/
12192 12374
 }@t\kern-3pt@>;
12193 12375
 
12194 12376
 @ In order to make this class work a little more nicely as an item delegate,
@@ -12221,12 +12403,25 @@ void SqlComboBox::showData(bool show)
12221 12403
 @ Next, there is a need to know if the NULL value may legally be selected. Where
12222 12404
 this is the case, we generally want this to be inserted first. As the
12223 12405
 |QComboBox| supports storing both display and user data, much of the code is a
12224
-thin wrapper around calls to the base class.
12406
+thin wrapper around calls to the base class. The text and data for the NULL
12407
+value can be set arbitrarily, which can be useful in certain cases. Note that
12408
+any customization of the NULL text or data must be set before a call to
12409
+|addNullOption()|.
12225 12410
 
12226 12411
 @<SqlComboBox Implementation@>=
12227 12412
 void SqlComboBox::addNullOption()
12228 12413
 {
12229
-	addItem(tr("Unknown"), QVariant(QVariant::String));
12414
+	addItem(specialNullText, specialNullData);
12415
+}
12416
+
12417
+void SqlComboBox::setNullText(QString nullText)
12418
+{
12419
+	specialNullText = nullText;
12420
+}
12421
+
12422
+void SqlComboBox::setNullData(QVariant nullData)
12423
+{
12424
+	specialNullData = nullData;
12230 12425
 }
12231 12426
 
12232 12427
 @ Typically, the SQL query used to populate this widget will request two columns
@@ -12253,7 +12448,8 @@ the combo box with the results.
12253 12448
 @<SqlComboBox Implementation@>=
12254 12449
 void SqlComboBox::addSqlOptions(QString query)
12255 12450
 {
12256
-	SqlQueryConnection *dbquery = new SqlQueryConnection;
12451
+	SqlQueryConnection h;
12452
+	QSqlQuery *dbquery = h.operator->();
12257 12453
 	if(!dbquery->exec(query))
12258 12454
 	{
12259 12455
 		QSqlError error = dbquery->lastError();
@@ -12273,7 +12469,6 @@ void SqlComboBox::addSqlOptions(QString query)
12273 12469
 		}
12274 12470
 		addItem(displayValue, dataValue);
12275 12471
 	}
12276
-	delete dbquery;
12277 12472
 }
12278 12473
 
12279 12474
 @ The constructor initializes some private member data. A size policy is also
@@ -12288,7 +12483,8 @@ The destructor is trivial.
12288 12483
 
12289 12484
 @<SqlComboBox Implementation@>=
12290 12485
 SqlComboBox::SqlComboBox() :
12291
-	dataColumn(0), displayColumn(0), dataColumnShown(false)
12486
+	dataColumn(0), displayColumn(0), dataColumnShown(false),
12487
+	specialNullText(tr("Unknown")), specialNullData(QVariant::String)
12292 12488
 {
12293 12489
 	view()->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
12294 12490
 }
@@ -12562,6 +12758,7 @@ void SqlConnectionSetup::testConnection()
12562 12758
 		settings.setValue("database/dbname", dbname->text());
12563 12759
 		settings.setValue("database/user", user->text());
12564 12760
 		settings.setValue("database/password", password->text());
12761
+		database.close();
12565 12762
 		accept();
12566 12763
 	}
12567 12764
 	else
@@ -12597,6 +12794,10 @@ if(!database.open())
12597 12794
 {
12598 12795
 	settings.setValue("database/exists", "false");
12599 12796
 }
12797
+else
12798
+{
12799
+	database.close();
12800
+}
12600 12801
 
12601 12802
 @** Viewing a record of batches.
12602 12803
 
@@ -12721,8 +12922,6 @@ void SqlQueryView::openRow(const QModelIndex &index)
12721 12922
 
12722 12923
 @ The other functions are wrappers around model methods.
12723 12924
 
12724
-\danger |setQuery()| leaks database connections.
12725
-
12726 12925
 @<SqlQueryView implementation@>=
12727 12926
 void SqlQueryView::setQuery(const QString &query)
12728 12927
 {
@@ -12730,6 +12929,7 @@ void SqlQueryView::setQuery(const QString &query)
12730 12929
 	database.open();
12731 12930
 	QSqlQuery q(query, database);
12732 12931
 	((QSqlQueryModel*)model())->setQuery(q);
12932
+	database.close();
12733 12933
 }
12734 12934
 
12735 12935
 bool SqlQueryView::setHeaderData(int section, Qt::Orientation@, orientation,
@@ -12841,10 +13041,18 @@ presently distributed with Typica, the approach taken to implementing this menu
12841 13041
 type is highly inefficient. There are many optimizations available if this
12842 13042
 becomes problematic.\endanger
12843 13043
 
13044
+When a report menu is generated, the directory used for this is added as a
13045
+search path for the |"reports"| prefix. This is used by the |createReport()|
13046
+script method and is intended to allow access to reports from outside of the
13047
+Report menu.
13048
+
12844 13049
 @<Populate reports menu@>=
12845 13050
 QSettings settings;
12846
-QDir directory(QString("%1/%2").arg(settings.value("config").toString()).
12847
-                                arg(element.attribute("src")));
13051
+QString reportDirectory = QString("%1/%2").arg(settings.value("config").
13052
+                                               toString()).
13053
+                                           arg(element.attribute("src"));
13054
+QDir::addSearchPath("reports", reportDirectory);
13055
+QDir directory(reportDirectory);
12848 13056
 directory.setFilter(QDir::Files);
12849 13057
 directory.setSorting(QDir::Name);
12850 13058
 QStringList nameFilter;
@@ -13191,36 +13399,37 @@ placeholders that have not yet had values bound to them), there will be no
13191 13399
 change to the table and the next child element, if any, will be processed.
13192 13400
 
13193 13401
 @<Add SQL query results to report table@>=
13194
-SqlQueryConnection query;
13195
-query.prepare(currentElement.text());
13402
+SqlQueryConnection h;
13403
+QSqlQuery *query = h.operator->();
13404
+query->prepare(currentElement.text());
13196 13405
 foreach(QString key, bindings.uniqueKeys())
13197 13406
 {
13198 13407
 	if(currentElement.text().contains(key))
13199 13408
 	{
13200
-		query.bindValue(key, bindings.value(key));
13409
+		query->bindValue(key, bindings.value(key));
13201 13410
 	}
13202 13411
 }
13203
-query.exec();
13204
-if(!query.next())
13412
+query->exec();
13413
+if(!query->next())
13205 13414
 {
13206 13415
 	continue;
13207 13416
 }
13208
-if(query.record().count() > columns)
13417
+if(query->record().count() > columns)
13209 13418
 {
13210
-	table->appendColumns(query.record().count() - columns);
13419
+	table->appendColumns(query->record().count() - columns);
13211 13420
 }
13212 13421
 do
13213 13422
 {
13214 13423
 	table->appendRows(1);
13215 13424
 	rows++;
13216 13425
 	currentRow++;
13217
-	for(int j = 0; j < query.record().count(); j++)
13426
+	for(int j = 0; j < query->record().count(); j++)
13218 13427
 	{
13219 13428
 		QTextTableCell cell = table->cellAt(currentRow, j);
13220 13429
 		cursor = cell.firstCursorPosition();
13221
-		cursor.insertText(query.value(j).toString());
13430
+		cursor.insertText(query->value(j).toString());
13222 13431
 	}
13223
-} while(query.next());
13432
+} while(query->next());
13224 13433
 
13225 13434
 @ It is sometimes desirable to add fixed data such as column headers to a table.
13226 13435
 This is done with the {\tt <row>} element.

Loading…
Cancel
Save