Typica is a free program for professional coffee roasters. https://typica.us
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  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 = TTR("manualLogEntry", "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. pluginContext.newMeasurement = function(m, c) {
  174. pluginContext.table.newMeasurement(m, c);
  175. pluginContext.graph.newMeasurement(m, c);
  176. }
  177. pluginMenu = findChildObject(this, 'pluginMenu');
  178. pluginMenu.setProperty("activationObject", pluginContext);
  179. tabs = findChildObject(this, 'tabs');
  180. tabs.addTab("Batch Data");
  181. tabs.addTab("Roast Data");
  182. pages = findChildObject(this, 'pages');
  183. tabs.currentChanged.connect(function(index) {
  184. pages.setCurrentIndex(index);
  185. });
  186. greenInfoLayout = findChildObject(this, 'greenInfoLayout');
  187. roastedItem = findChildObject(this, 'roastedItem');
  188. batchType = findChildObject(this, 'batchType');
  189. batchType.addItem("Sample");
  190. batchType.addItem("Production");
  191. batchType['currentIndexChanged(int)'].connect(function(batchTypeIndex) {
  192. QSettings.setValue("script/manual_batchType", batchTypeIndex);
  193. greenInfoLayout.setCurrentIndex(batchTypeIndex);
  194. roastedItem.enabled = (batchTypeIndex == 1);
  195. });
  196. batchType.setCurrentIndex(QSettings.value("script/manual_batchType", 1));
  197. var machineSelector = findChildObject(this, 'machineSelector');
  198. var machineModel = new DeviceTreeModel;
  199. machineSelector.setModel(machineModel);
  200. machineSelector.currentIndex = QSettings.value("script/manualMachineSelection", 0);
  201. machineSelector['currentIndexChanged(int)'].connect(function(index) {
  202. QSettings.setValue("script/manualMachineSelection", index);
  203. });
  204. sampleGreenUnit = findChildObject(this, 'sampleGreenUnit');
  205. sampleGreenUnit.addItem("g");
  206. sampleGreenUnit.addItem("Kg");
  207. sampleGreenUnit.addItem("oz");
  208. sampleGreenUnit.addItem("lb");
  209. sampleGreenUnit.currentIndex = (QSettings.value("script/manual_unit", sampleGreenUnit.findText("lb")));
  210. productionGreenUnit = findChildObject(this, 'productionGreenUnit');
  211. productionGreenUnit.addItem("g");
  212. productionGreenUnit.addItem("Kg");
  213. productionGreenUnit.addItem("oz");
  214. productionGreenUnit.addItem("lb");
  215. productionGreenUnit.currentIndex = (QSettings.value("script/manual_unit", productionGreenUnit.findText("lb")));
  216. sampleGreenUnit['currentIndexChanged(int)'].connect(function(greenUnitIndex) {
  217. QSettings.setValue("script/manual_unit", greenUnitIndex);
  218. productionGreenUnit.setCurrentIndex(greenUnitIndex);
  219. });
  220. productionGreenUnit['currentIndexChanged(int)'].connect(function(greenUnitIndex) {
  221. QSettings.setValue("script/manual_unit", greenUnitIndex);
  222. sampleGreenUnit.setCurrentIndex(greenUnitIndex);
  223. });
  224. timeincrement = findChildObject(this, 'timeincrement');
  225. currenttime = findChildObject(this, 'currenttime');
  226. currenttemperature = findChildObject(this, 'currenttemperature');
  227. currentnote = findChildObject(this, 'currentnote');
  228. addmeasurement = findChildObject(this, 'addmeasurement');
  229. var currentUnit = Units.Fahrenheit;
  230. var showC = findChildObject(this, 'showC');
  231. showC.triggered.connect(function() {
  232. pluginContext.table.setDisplayUnits(Units.Celsius);
  233. pluginContext.graph.showC();
  234. QSettings.setValue("temperatureUnit", "C");
  235. currentUnit = Units.Celsius;
  236. });
  237. var showF = findChildObject(this, 'showF');
  238. showF.triggered.connect(function() {
  239. pluginContext.table.setDisplayUnits(Units.Fahrenheit);
  240. pluginContext.graph.showF();
  241. QSettings.setValue("temperatureUnit", "F");
  242. currentUnit = Units.Fahrenheit;
  243. });
  244. if(QSettings.value("temperatureUnit", "F") == "C") {
  245. showC.trigger();
  246. }
  247. addmeasurement.clicked.connect(function() {
  248. var fromUnit =
  249. pluginContext.newMeasurement(new Measurement(Units.convertTemperature(Number(currenttemperature.text), currentUnit, Units.Fahrenheit), currenttime.time), 1);
  250. if(currentnote.text.length > 0) {
  251. pluginContext.table.newAnnotation(currentnote.text, 1, 2);
  252. }
  253. currentnote.text = "";
  254. var t = QTime();
  255. t = t.fromString(currenttime.time, "hh:mm:ss");
  256. t = t.addSecs(30);
  257. currenttime.time = t;
  258. currenttemperature.text = "";
  259. });
  260. currenttemperature.returnPressed.connect(addmeasurement.clicked);
  261. currentnote.returnPressed.connect(addmeasurement.clicked);
  262. var v1 = findChildObject(this, 'ms');
  263. v1.triggered.connect(pluginContext.table.LOD_ms);
  264. var v2 = findChildObject(this, '1s');
  265. v2.triggered.connect(pluginContext.table.LOD_1s);
  266. var v3 = findChildObject(this, '5s');
  267. v3.triggered.connect(pluginContext.table.LOD_5s);
  268. var v4 = findChildObject(this, '10s');
  269. v4.triggered.connect(pluginContext.table.LOD_10s);
  270. var v5 = findChildObject(this, '15s');
  271. v5.triggered.connect(pluginContext.table.LOD_15s);
  272. var v6 = findChildObject(this, '30s');
  273. v6.triggered.connect(pluginContext.table.LOD_30s);
  274. var v7 = findChildObject(this, '1m');
  275. v7.triggered.connect(pluginContext.table.LOD_1m);
  276. var clear = findChildObject(this, 'clear');
  277. clear.triggered.connect(pluginContext.table.clear);
  278. clear.triggered.connect(pluginContext.graph.clear);
  279. clear.triggered.connect(function() {
  280. currenttime.time = QTime(0, 0, 0, 0);
  281. currenttemperature.text = "";
  282. currentnote.text = "";
  283. pluginContext.table.clearOutputColumns();
  284. pluginContext.table.addOutputTemperatureColumn(1);
  285. pluginContext.table.addOutputAnnotationColumn(2);
  286. });
  287. var sampleGreenName = findChildObject(this, 'sampleGreenName');
  288. var sampleGreenWeight = findChildObject(this, 'sampleGreenWeight');
  289. var productionGreenTable = findChildObject(this, 'productionGreenTable');
  290. var greenModel = productionGreenTable.model();
  291. var greenTotal = 0.0;
  292. var updateGreenTable = function() {
  293. var deleteRow = -1;
  294. while((deleteRow = productionGreenTable.findData("delete", 0)) > -1) {
  295. if(productionGreenTable.data(deleteRow, 0, 0) == "Delete") {
  296. productionGreenTable.removeRow(productionGreenTable.findData("delete", 0));
  297. } else {
  298. break;
  299. }
  300. }
  301. greenTotal = productionGreenTable.columnSum(1, 0);
  302. productionGreenTable.resizeColumnToContents(0);
  303. };
  304. greenModel.dataChanged.connect(updateGreenTable);
  305. var validateInputs = function() {
  306. if(batchType.currentIndex == 0) {
  307. /* Sample batch */
  308. if(sampleGreenName.text.length == 0) {
  309. tabs.setCurrentIndex(0);
  310. displayError(TTR("manualLogEntry", "Data Entry Error"),
  311. TTR("manualLogEntry", "Please enter a green coffee name."));
  312. return false;
  313. }
  314. if(Number(sampleGreenWeight.text) <= 0 || isNaN(sampleGreenWeight.text)) {
  315. tabs.setCurrentIndex(0);
  316. displayError(TTR("manualLogEntry", "Data Entry Error"),
  317. TTR("manualLogEntry", "Green coffee weight must be a number greater than 0."));
  318. return false;
  319. }
  320. } else {
  321. /* Production batch */
  322. var itemArray = productionGreenTable.columnArray(0, 32).split("\\s*,\\s*");
  323. var weightArray = productionGreenTable.columnArray(1, 0).split("\\s*,\\s*");
  324. if((itemArray.length != weightArray.length) || (itemArray.length == 0)) {
  325. tabs.setCurrentIndex(0);
  326. displayError(TTR("manualLogEntry", "Data Entry Error"),
  327. TTR("manualLogEntry", "Please check that at least one green coffee has been selected and each green coffee has a valid weight"));
  328. return false;
  329. }
  330. if(Number(greenTotal) <= 0) {
  331. tabs.setCurrentIndex(0);
  332. displayError(TTR("manualLogEntry", "DataEntryError"),
  333. TTR("manualLogEntry", "Total green coffee weight must be a number greater than 0."));
  334. return false;
  335. }
  336. if(roastedItem.currentIndex == 0) {
  337. tabs.setCurrentIndex(0);
  338. displayError(TTR("manualLogEntry", "DataEntryError"),
  339. TTR("manualLogEntry", "Please select a roasted coffee item."));
  340. return false;
  341. }
  342. }
  343. return true;
  344. };
  345. var roastDataExists = function() {
  346. return (pluginContext.table.rowCount() > 0);
  347. }
  348. var roastTime = findChildObject(this, 'roastTime');
  349. var attributes = findChildObject(this, 'attributes');
  350. var sampleGreenArrivalDate = findChildObject(this, 'sampleGreenArrivalDate');
  351. var convertToPounds = function(w, u) {
  352. switch(u) {
  353. case "g":
  354. return w * 0.0022;
  355. case "oz":
  356. return w * 0.0625;
  357. case "Kg":
  358. return w * 2.2;
  359. }
  360. return w;
  361. };
  362. var roastedWeight = findChildObject(this, 'roastedWeight');
  363. var notes = findChildObject(this, 'notes');
  364. var roastDuration = findChildObject(this, 'roastDuration');
  365. var doSubmit = function() {
  366. var fileID = -1;
  367. var query = new QSqlQuery();
  368. if(roastDataExists()) {
  369. var buffer = new QBuffer;
  370. buffer.open(3);
  371. pluginContext.table.saveXML(buffer);
  372. buffer.open(3); /* saveXML closes the buffer */
  373. var q = "INSERT INTO files (id, name, type, note, file) VALUES (default, :name, 'profile', NULL, :data) RETURNING id";
  374. query.prepare(q);
  375. query.bind(":name", roastTime.text + " Manual Entry");
  376. query.bind(":data", buffer.readToString());
  377. query.exec();
  378. query.next();
  379. fileID = query.value(0);
  380. buffer.close();
  381. }
  382. var rootIndex = machineModel.index(machineSelector.currentIndex, 0);
  383. var selectedRoasterName = machineModel.data(rootIndex, 0);
  384. var machineReference = machineModel.referenceElement(machineModel.data(rootIndex, 32));
  385. var selectedRoasterID = machineReference.databaseid;
  386. query.exec("SELECT 1 FROM machine WHERE id = " + selectedRoasterID);
  387. if(!query.next()) {
  388. query.prepare("INSERT INTO machine (id, name) VALUES (:id, :name)");
  389. query.bind(":id", selectedRoasterID);
  390. query.bind(":name", selectedRoasterName);
  391. query.exec();
  392. }
  393. if(batchType.currentIndex == 0) {
  394. /* Sample roast */
  395. var attnames = sqlToArray(attributes.columnArray(0, 0));
  396. for(var i = 0; i < attnames.length; i++) {
  397. var attname = attnames[i];
  398. if(attname[0] == '{') {
  399. attname = attname.substr(1);
  400. }
  401. if(attname[0] == ' ') {
  402. attname = attname.substr(1);
  403. }
  404. if(attname[attname.length - 1] == '}') {
  405. attname = attname.substr(0, attname.length - 1);
  406. }
  407. if(attname.length == 0) {
  408. break;
  409. }
  410. query.prepare("SELECT id FROM item_attributes WHERE name = :name");
  411. query.bind(":name", attname);
  412. query.exec();
  413. if(query.next()) {
  414. attributes.setData(i, 0, query.value(0), 32);
  415. } else {
  416. query.prepare("INSERT INTO item_attributes(id, name) VALUES (DEFAULT, :name) RETURNING id");
  417. query.bind(":name", attname);
  418. query.exec();
  419. query.next();
  420. attributes.setData(i, 0, query.value(0), 32);
  421. }
  422. }
  423. 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");
  424. query.bind(":name", sampleGreenName.text);
  425. query.bind(":arrival", sampleGreenArrivalDate.date);
  426. query.bind(":attrids", attributes.bindableColumnArray(0, 32));
  427. query.bind(":attrvals", attributes.bindableQuotedColumnArray(1, 0));
  428. query.exec();
  429. query.next();
  430. var greenId = query.value(0);
  431. query.prepare("INSERT INTO items (id, name, reference, unit, quantity, category) VALUES (DEFAULT, :name, NULL, 'lb', 0, 'Coffee: Roasted Sample') RETURNING id");
  432. query.bind(":name", sampleGreenName.text + " Roasted Sample");
  433. query.exec();
  434. query.next();
  435. var roastedId = query.value(0);
  436. 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)");
  437. query.bind(":time", roastTime.text);
  438. query.bind(":unroastedids", "{" + greenId + "}");
  439. query.bind(":greens", "{" + convertToPounds(parseFloat(sampleGreenWeight.text), sampleGreenUnit.currentText) + "}");
  440. query.bind(":green", convertToPounds(parseFloat(sampleGreenWeight.text), sampleGreenUnit.currentText));
  441. query.bind("roastedid", Number(roastedId));
  442. query.bind("roasted", convertToPounds(parseFloat(roastedWeight.text), sampleGreenUnit.currentText));
  443. query.bind(":note", notes.plainText);
  444. query.bind(":machine", Number(selectedRoasterID));
  445. query.bind(":duration", roastDuration.text);
  446. if(fileID > 0) {
  447. query.bind(":files", "{" + fileID + "}");
  448. } else {
  449. query.bind(":file", "{}");
  450. }
  451. query.bind(":user", Application.currentTypicaUser());
  452. query.exec();
  453. } else {
  454. 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, ";
  455. q += productionGreenTable.columnArray(0, 32);
  456. q += ", ";
  457. var greenSum = 0.0;
  458. for(var i = 0; i < productionGreenTable.data(i, 1, 0).value != ""; i++) {
  459. var greenWt = convertToPounds(parseFloat(productionGreenTable.data(i, 1, 0)), productionGreenUnit.currentText);
  460. productionGreenTable.setData(i, 1, greenWt, 32);
  461. greenSum += greenWt;
  462. }
  463. q += productionGreenTable.columnArray(1, 32);
  464. q += ", ";
  465. q += greenSum;
  466. q += ", ";
  467. q += roastedItem.currentData();
  468. q += ", ";
  469. q += convertToPounds(parseFloat(roastedWeight.text), productionGreenUnit.currentText);
  470. q += ", 'ROAST', :annotation, ";
  471. q += selectedRoasterID;
  472. q += ", :duration, TRUE, NULL, NULL, NULL, NULL, '{";
  473. if(fileID > 0) {
  474. q += fileID;
  475. }
  476. q += "}', :user)";
  477. query.prepare(q);
  478. query.bind(":time", roastTime.text);
  479. query.bind(":annotation", notes.plainText);
  480. query.bind(":duration", roastDuration.text);
  481. query.bind(":user", Application.currentTypicaUser());
  482. query.exec();
  483. }
  484. query = query.invalidate();
  485. window.close();
  486. }
  487. var submit = findChildObject(this, 'submit');
  488. submit.clicked.connect(function() {
  489. if(validateInputs()) {
  490. doSubmit();
  491. }
  492. });
  493. ]]>
  494. </program>
  495. </window>