src/EditProductTab7.cpp

changeset 175
f1ed3a2a94e9
child 177
62b8d701cd88
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/EditProductTab7.cpp	Thu Apr 28 22:49:13 2022 +0200
@@ -0,0 +1,682 @@
+/**
+ * EditProduct.cpp is part of bmsapp.
+ *
+ * tab 7, mash.
+ *
+ * bmsapp is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * bmsapp is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+void EditProduct::refreshMashs()
+{
+    QString w;
+    QWidget* pWidget;
+    QHBoxLayout* pLayout;
+    QTableWidgetItem *item;
+    QIcon down_icon, up_icon;
+
+    qDebug() << "refreshMashs" << product->mashs.size();
+
+    down_icon.addFile(QString::fromUtf8(":/icons/silk/bullet_arrow_down.png"), QSize(), QIcon::Normal, QIcon::Off);
+    up_icon.addFile(QString::fromUtf8(":/icons/silk/bullet_arrow_up.png"), QSize(), QIcon::Normal, QIcon::Off);
+
+    const QStringList labels({tr("Step name"), tr("Type"), tr("Start"), tr("End"), tr("Rest"), tr("Ramp"),
+			      tr("Inf/dec"), tr("Inf/dec"), tr("Volume"), tr("W/G ratio"), "", "", tr("Delete"), tr("Edit") });
+
+    ui->mashsTable->setColumnCount(14);
+    ui->mashsTable->setColumnWidth(0, 189);	/* Step name	*/
+    ui->mashsTable->setColumnWidth(1, 100);	/* Type		*/
+    ui->mashsTable->setColumnWidth(2,  70);	/* Start temp	*/
+    ui->mashsTable->setColumnWidth(3,  70);	/* End temp	*/
+    ui->mashsTable->setColumnWidth(4,  70);	/* Rest time	*/
+    ui->mashsTable->setColumnWidth(5,  70);	/* Ramp time	*/
+    ui->mashsTable->setColumnWidth(6,  70);	/* Infusion vol	*/
+    ui->mashsTable->setColumnWidth(7,  70);     /* Infusion tmp	*/
+    ui->mashsTable->setColumnWidth(8,  70);	/* Volume	*/
+    ui->mashsTable->setColumnWidth(9,  80);	/* W/G ratio	*/
+    ui->mashsTable->setColumnWidth(10, 30);	/* Up button	*/
+    ui->mashsTable->setColumnWidth(11, 30);	/* Down button	*/
+    ui->mashsTable->setColumnWidth(12, 80);	/* Delete	*/
+    ui->mashsTable->setColumnWidth(13, 80);	/* Edit		*/
+    ui->mashsTable->setHorizontalHeaderLabels(labels);
+    ui->mashsTable->verticalHeader()->hide();
+    ui->mashsTable->setRowCount(product->mashs.size());
+
+    for (int i = 0; i < product->mashs.size(); i++) {
+
+	ui->mashsTable->setItem(i, 0, new QTableWidgetItem(product->mashs.at(i).step_name));
+
+	item = new QTableWidgetItem(step_types[product->mashs.at(i).step_type]);
+        item->setTextAlignment(Qt::AlignCenter|Qt::AlignVCenter);
+        ui->mashsTable->setItem(i, 1, item);
+
+	item = new QTableWidgetItem(QString("%1 °C").arg(product->mashs.at(i).step_temp, 2, 'f', 1, '0'));
+	item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
+	ui->mashsTable->setItem(i, 2, item);
+
+	item = new QTableWidgetItem(QString("%1 °C").arg(product->mashs.at(i).end_temp, 2, 'f', 1, '0'));
+        item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
+        ui->mashsTable->setItem(i, 3, item);
+
+	item = new QTableWidgetItem(QString("%1 min").arg(product->mashs.at(i).step_time, 1, 'f', 0, '0'));
+        item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
+        ui->mashsTable->setItem(i, 4, item);
+
+	item = new QTableWidgetItem(QString("%1 min").arg(product->mashs.at(i).ramp_time, 1, 'f', 0, '0'));
+        item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
+        ui->mashsTable->setItem(i, 5, item);
+
+	if (product->mashs.at(i).step_infuse_amount > 0) {
+	    item = new QTableWidgetItem(QString("%1 L").arg(product->mashs.at(i).step_infuse_amount, 2, 'f', 1, '0'));
+            item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
+            ui->mashsTable->setItem(i, 6, item);
+	    item = new QTableWidgetItem(QString("%1 °C").arg(product->mashs.at(i).step_infuse_temp, 3, 'f', 2, '0'));
+            item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
+            ui->mashsTable->setItem(i, 7, item);
+	} else {
+	    ui->mashsTable->removeCellWidget(i, 6);
+	    ui->mashsTable->removeCellWidget(i, 7);
+	}
+
+	item = new QTableWidgetItem(QString("%1 L").arg(product->mashs.at(i).step_volume, 2, 'f', 1, '0'));
+        item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
+        ui->mashsTable->setItem(i, 8, item);
+
+	item = new QTableWidgetItem(QString("%1 L/kg").arg(product->mashs.at(i).step_wg_ratio, 3, 'f', 2, '0'));
+        item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
+        ui->mashsTable->setItem(i, 9, item);
+
+	if (i > 0) {
+	    pWidget = new QWidget();
+            QPushButton* btn_up = new QPushButton();
+            btn_up->setObjectName(QString("%1").arg(i));  /* Send row with the button */
+            btn_up->setIcon(up_icon);
+            connect(btn_up, SIGNAL(clicked()), this, SLOT(upMashRow_clicked()));
+            pLayout = new QHBoxLayout(pWidget);
+            pLayout->addWidget(btn_up);
+            pLayout->setContentsMargins(5, 0, 5, 0);
+            pWidget->setLayout(pLayout);
+            ui->mashsTable->setCellWidget(i, 10, pWidget);
+	} else {
+	    ui->mashsTable->removeCellWidget(i, 10);
+	}
+
+	if (i < (product->mashs.size() - 1)) {
+	    pWidget = new QWidget();
+            QPushButton* btn_down = new QPushButton();
+            btn_down->setObjectName(QString("%1").arg(i));  /* Send row with the button */
+	    btn_down->setIcon(down_icon);
+            connect(btn_down, SIGNAL(clicked()), this, SLOT(downMashRow_clicked()));
+            pLayout = new QHBoxLayout(pWidget);
+            pLayout->addWidget(btn_down);
+            pLayout->setContentsMargins(5, 0, 5, 0);
+            pWidget->setLayout(pLayout);
+            ui->mashsTable->setCellWidget(i, 11, pWidget);
+	} else {
+	    ui->mashsTable->removeCellWidget(i, 11);
+	}
+
+	pWidget = new QWidget();
+        QPushButton* btn_dele = new QPushButton();
+        btn_dele->setObjectName(QString("%1").arg(i));  /* Send row with the button */
+        btn_dele->setText(tr("Delete"));
+        connect(btn_dele, SIGNAL(clicked()), this, SLOT(deleteMashRow_clicked()));
+        pLayout = new QHBoxLayout(pWidget);
+        pLayout->addWidget(btn_dele);
+        pLayout->setContentsMargins(5, 0, 5, 0);
+        pWidget->setLayout(pLayout);
+        ui->mashsTable->setCellWidget(i, 12, pWidget);
+
+        pWidget = new QWidget();
+        QPushButton* btn_edit = new QPushButton();
+        btn_edit->setObjectName(QString("%1").arg(i));  /* Send row with the button */
+        btn_edit->setText(tr("Edit"));
+        connect(btn_edit, SIGNAL(clicked()), this, SLOT(editMashRow_clicked()));
+        pLayout = new QHBoxLayout(pWidget);
+        pLayout->addWidget(btn_edit);
+        pLayout->setContentsMargins(5, 0, 5, 0);
+        pWidget->setLayout(pLayout);
+        ui->mashsTable->setCellWidget(i, 13, pWidget);
+    }
+}
+
+
+double EditProduct::infusionVol(double step_infused, double step_mashkg, double infuse_temp, double step_temp, double last_temp)
+{
+    double a = last_temp * (equip_tun_weight * equip_tun_specific_heat + step_infused * SpecificHeatWater + step_mashkg * SpecificHeatMalt);
+    double b = step_temp * (equip_tun_weight * equip_tun_specific_heat + step_infused * SpecificHeatWater + step_mashkg * SpecificHeatMalt);
+    double vol = round(((b - a) / ((infuse_temp - step_temp) * SpecificHeatWater)) * 100.0) / 100.0;
+
+    if (vol < 0)
+	vol = 0;
+    qDebug() << "  infusionVol(" << step_infused << "," << step_mashkg << "," << infuse_temp <<"," << step_temp << "," << last_temp << "):" << vol;
+    return vol;
+}
+
+
+double EditProduct::decoctionVol(double step_volume, double step_temp, double prev_temp)
+{
+    double a = (equip_tun_weight * equip_tun_specific_heat + step_volume * SpecificHeatWater) * (step_temp - prev_temp);
+    double b = SpecificHeatWater * (99 - step_temp);
+    double vol = 0;
+
+    if (b > 0)
+	vol = round((a / b) * 1000000.0) / 1000000.0;
+    qDebug() << "  decoctionVol(" << step_volume << "," << step_temp << "," << prev_temp << "):" << vol;
+    return vol;
+}
+
+
+void EditProduct::calcMash()
+{
+    double infused = 0, vol, a, b, temp;
+    int    i, j, n;
+    double lasttemp = 18.0;
+    double graintemp = 18.0;
+    double tuntemp = 18.0;
+
+    product->mashs_time = 0;
+
+    if (product->mashs.size() && product->mashs_kg > 0) {
+	qDebug() << "calcMash()";
+
+	for (i = 0; i < product->mashs.size(); i++) {
+	    if (product->mashs.at(i).step_type == 0) { // Infusion
+		if (i == 0) {
+		    // First mash step, temperature from the mashtun and malt.
+		    n = 20; // tun is preheated.
+		    tuntemp = product->mashs.at(i).step_temp;
+		    for (j = 0; j < n; j++) {
+			a = product->mashs_kg * graintemp * SpecificHeatMalt + equip_tun_weight * tuntemp * equip_tun_specific_heat;
+			b = product->mashs[i].step_temp *
+			      (equip_tun_weight * equip_tun_specific_heat +
+			       product->mashs.at(i).step_infuse_amount * SpecificHeatWater + 
+			       product->mashs_kg * SpecificHeatMalt) -
+			      SlakingHeat * product->mashs_kg;
+			if (product->mashs.at(i).step_infuse_amount > 0) {
+			    temp = (b - a) / (product->mashs.at(i).step_infuse_amount * SpecificHeatWater);
+			} else {
+			    temp = 99;
+			}
+			tuntemp += (temp - tuntemp) / 2;
+			product->mashs[i].step_infuse_temp = round(temp * 1000000.0) / 1000000.0;
+		    }
+		    qDebug() << "  init infuse temp:" << product->mashs.at(i).step_infuse_temp;
+		} else {
+		    // Calculate amount of infusion water.
+		    product->mashs[i].step_infuse_amount =
+			    infusionVol(infused, product->mashs_kg, product->mashs.at(i).step_infuse_temp, product->mashs.at(i).step_temp, lasttemp);
+		    qDebug() << i << "  vol:" << product->mashs.at(i).step_infuse_amount << "temp:" << product->mashs.at(i).step_infuse_temp;
+		}
+		infused += product->mashs.at(i).step_infuse_amount;
+	    } else if (product->mashs.at(i).step_type == 1) { // Temperature
+		if (i > 0)
+		    product->mashs[i].step_infuse_amount = 0;
+		product->mashs[i].step_infuse_temp = 0;
+	    } else if (product->mashs.at(i).step_type == 2) { // Decoction
+		product->mashs[i].step_infuse_amount = decoctionVol(infused, product->mashs.at(i).step_temp, lasttemp);
+		product->mashs[i].step_infuse_temp = 99;
+	    }
+	    product->mashs[i].step_volume = infused;
+	    //qDebug() << i << "  type:" << product->mashs.at(i).step_type << "volume:" << product->mashs.at(i).step_infuse_amount << "temp:" << product->mashs.at(i).step_infuse_temp;
+	    lasttemp = product->mashs.at(i).step_temp;
+	    product->mashs_time += product->mashs.at(i).step_time;
+	    if (i > 0)
+	    	product->mashs_time += product->mashs.at(i).ramp_time;
+	    product->mashs[i].step_wg_ratio = round((infused / product->mashs_kg) * 1000000.0) / 1000000.0;
+	}
+    }
+
+    /* Show the calculated total mash time. */
+    ui->mash_timeEdit->setText(QString("%1:%2").arg(product->mashs_time / 60).arg(product->mashs_time % 60, 2, 'f', 0, '0'));
+}
+
+
+void EditProduct::addMashRow_clicked()
+{
+    Mashs newm;
+
+    for (int i = 0; i < product->mashs.size(); i++) {
+        if (product->mashs.at(i).step_time == 0)
+            return;     // Add only one at a time.
+    }
+
+    newm.step_name = "Name me";
+    newm.step_temp = newm.end_temp = 67.0;
+    newm.step_time = 20;
+    newm.ramp_time = 10;
+    if (product->mashs.size()) {
+	newm.step_volume = product->mashs.at(product->mashs.size() - 1).step_volume;
+	newm.step_wg_ratio = product->mashs.at(product->mashs.size() - 1).step_wg_ratio;
+	newm.step_type = product->mashs.at(product->mashs.size() - 1).step_type;
+    } else {
+	newm.step_volume = 0;
+	newm.step_wg_ratio = 0;
+	newm.step_type = 1;
+    }
+    newm.step_infuse_amount = newm.step_infuse_temp = 0;
+
+    product->mashs.append(newm);
+    is_changed();
+    emit refreshAll();
+}
+
+
+void EditProduct::deleteMashRow_clicked()
+{
+    if (product->locked || product->mashs.size() < 1)
+	return;
+
+    QPushButton *pb = qobject_cast<QPushButton *>(QObject::sender());
+    int row = pb->objectName().toInt();
+    qDebug() << "Delete mash row" << row << product->mashs.size();
+
+    int rc = QMessageBox::warning(this, tr("Delete mash step"), tr("Delete %1").arg(product->mashs.at(row).step_name),
+                    QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
+    if (rc == QMessageBox::No)
+        return;
+
+    product->mashs.removeAt(row);
+    is_changed();
+    emit refreshAll();
+}
+
+
+void EditProduct::upMashRow_clicked()
+{
+    if (product->locked || product->mashs.size() < 1)
+        return;
+
+    QPushButton *pb = qobject_cast<QPushButton *>(QObject::sender());
+    int row = pb->objectName().toInt();
+    qDebug() << "Move up mash row" << row << product->mashs.size();
+
+    Mashs temp;
+    temp = product->mashs[row - 1];
+    product->mashs[row - 1] = product->mashs[row];
+    product->mashs[row] = temp;
+    is_changed();
+    emit refreshAll();
+}
+
+
+void EditProduct::downMashRow_clicked()
+{
+    if (product->locked || product->mashs.size() < 1)
+        return;
+
+    QPushButton *pb = qobject_cast<QPushButton *>(QObject::sender());
+    int row = pb->objectName().toInt();
+    qDebug() << "Move down mash row" << row << product->mashs.size();
+
+    Mashs temp;
+    temp = product->mashs[row + 1];
+    product->mashs[row + 1] = product->mashs[row];
+    product->mashs[row] = temp;
+    is_changed();
+    emit refreshAll();
+}
+
+
+void EditProduct::step_name_changed(QString val)
+{
+    product->mashs[product->mashs_row].step_name = val;
+    ui->mashsTable->setItem(product->mashs_row, 0, new QTableWidgetItem(val));
+    is_changed();
+}
+
+
+void EditProduct::step_type_changed(int val)
+{
+    qDebug() << "step_type_changed" << product->mashs_row << val;
+
+    product->mashs[product->mashs_row].step_type = val;
+    ivolLabel->setVisible(product->mashs.at(product->mashs_row).step_type == 0);
+    stepivolEdit->setVisible(product->mashs.at(product->mashs_row).step_type == 0);
+    itmpLabel->setVisible(product->mashs.at(product->mashs_row).step_type == 0);
+    stepitmpEdit->setVisible(product->mashs.at(product->mashs_row).step_type == 0);
+    is_changed();
+    emit refreshAll();
+}
+
+
+void EditProduct::step_temp_changed(double val)
+{
+    qDebug() << "step_temp_changed" << product->mashs_row << val;
+    product->mashs[product->mashs_row].step_temp = val;
+    QTableWidgetItem *item = new QTableWidgetItem(QString("%1 °C").arg(val, 2, 'f', 1, '0'));
+    item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
+    ui->mashsTable->setItem(product->mashs_row, 2, item);
+    is_changed();
+    emit refreshAll();
+}
+
+
+void EditProduct::end_temp_changed(double val)
+{
+    qDebug() << "end_temp_changed" << product->mashs_row << val;
+    product->mashs[product->mashs_row].end_temp = val;
+    QTableWidgetItem *item = new QTableWidgetItem(QString("%1 °C").arg(val, 2, 'f', 1, '0'));
+    item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
+    ui->mashsTable->setItem(product->mashs_row, 3, item);
+    is_changed();
+    emit refreshAll();
+}
+
+
+void EditProduct::step_time_changed(double val)
+{
+    qDebug() << "step_time_changed" << product->mashs_row << val;
+    product->mashs[product->mashs_row].step_time = val;
+    QTableWidgetItem *item = new QTableWidgetItem(QString("%1 min").arg(val, 1, 'f', 0, '0'));
+    item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
+    ui->mashsTable->setItem(product->mashs_row, 4, item);
+    is_changed();
+    emit refreshAll();
+}
+
+
+void EditProduct::ramp_time_changed(double val)
+{
+    qDebug() << "ramp_time_changed" << product->mashs_row << val;
+    product->mashs[product->mashs_row].ramp_time = val;
+    QTableWidgetItem *item = new QTableWidgetItem(QString("%1 min").arg(val, 1, 'f', 0, '0'));
+    item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
+    ui->mashsTable->setItem(product->mashs_row, 5, item);
+    is_changed();
+    emit refreshAll();
+}
+
+
+void EditProduct::infuse_changed(double val)
+{
+    qDebug() << "infuse_changed" << product->mashs_row << val;
+    product->mashs[product->mashs_row].step_infuse_amount = val;
+    QTableWidgetItem *item = new QTableWidgetItem(QString("%1 L").arg(val, 1, 'f', 0, '0'));
+    item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
+    ui->mashsTable->setItem(product->mashs_row, 6, item);
+
+    /*
+     * Recalculate water volumes
+     */
+    double volume = 0;
+    for (int i = 0; i < product->mashs.size(); i++) {
+	if (product->mashs.at(i).step_type == 0) {
+	    volume += product->mashs.at(i).step_infuse_amount;
+	}
+	product->mashs[i].step_volume = volume;
+    }
+    product->w1_amount = volume - product->w2_amount;
+    product->wg_amount = volume;
+    ui->w1_volEdit->setValue(product->w1_amount);
+
+    is_changed();
+    emit refreshAll();
+}
+
+
+void EditProduct::editMashRow_clicked()
+{
+    QSqlQuery query;
+
+    if (product->locked)
+	return;
+
+    QPushButton *pb = qobject_cast<QPushButton *>(QObject::sender());
+    product->mashs_row = pb->objectName().toInt();
+    qDebug() << "Edit mash row" << product->mashs_row;
+    Mashs backup = product->mashs.at(product->mashs_row);
+
+    QDialog* dialog = new QDialog(this);
+    dialog->resize(738, 230);
+    QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog);
+    buttonBox->setObjectName(QString::fromUtf8("buttonBox"));
+    buttonBox->setGeometry(QRect(30, 180, 671, 32));
+    buttonBox->setLayoutDirection(Qt::LeftToRight);
+    buttonBox->setOrientation(Qt::Horizontal);
+    buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok);
+    buttonBox->setCenterButtons(true);
+
+    QLabel *nameLabel = new QLabel(dialog);
+    nameLabel->setObjectName(QString::fromUtf8("nameLabel"));
+    nameLabel->setText(tr("Step name:"));
+    nameLabel->setGeometry(QRect(10, 10, 141, 20));
+    nameLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+    stepnameEdit = new QLineEdit(dialog);
+    stepnameEdit->setObjectName(QString::fromUtf8("stepnameEdit"));
+    stepnameEdit->setText(product->mashs.at(product->mashs_row).step_name);
+    stepnameEdit->setGeometry(QRect(160, 10, 511, 23));
+
+    QLabel *typeLabel = new QLabel(dialog);
+    typeLabel->setObjectName(QString::fromUtf8("typeLabel"));
+    typeLabel->setText(tr("Step type:"));
+    typeLabel->setGeometry(QRect(10, 40, 141, 20));
+    typeLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+    QComboBox *typeEdit = new QComboBox(dialog);
+    typeEdit->setObjectName(QString::fromUtf8("typeEdit"));
+    typeEdit->setGeometry(QRect(160, 40, 161, 23));
+    typeEdit->addItem(tr("Infusion"));
+    typeEdit->addItem(tr("Temperature"));
+    typeEdit->addItem(tr("Decoction"));
+    typeEdit->setCurrentIndex(product->mashs.at(product->mashs_row).step_type);
+
+    QLabel *tempLabel = new QLabel(dialog);
+    tempLabel->setObjectName(QString::fromUtf8("tempLabel"));
+    tempLabel->setText(tr("Step start temp:"));
+    tempLabel->setGeometry(QRect(10, 70, 141, 20));
+    tempLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+    steptempEdit = new QDoubleSpinBox(dialog);
+    steptempEdit->setObjectName(QString::fromUtf8("steptempEdit"));
+    steptempEdit->setGeometry(QRect(160, 70, 121, 24));
+    steptempEdit->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+    steptempEdit->setDecimals(1);
+    steptempEdit->setValue(product->mashs.at(product->mashs_row).step_temp);
+
+    QLabel *endLabel = new QLabel(dialog);
+    endLabel->setObjectName(QString::fromUtf8("endLabel"));
+    endLabel->setText(tr("Step end temp:"));
+    endLabel->setGeometry(QRect(360, 70, 141, 20));
+    endLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+    endtempEdit = new QDoubleSpinBox(dialog);
+    endtempEdit->setObjectName(QString::fromUtf8("endtempEdit"));
+    endtempEdit->setGeometry(QRect(510, 70, 121, 24));
+    endtempEdit->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+    endtempEdit->setDecimals(1);
+    endtempEdit->setValue(product->mashs.at(product->mashs_row).end_temp);
+
+    QLabel *timeLabel = new QLabel(dialog);
+    timeLabel->setObjectName(QString::fromUtf8("timeLabel"));
+    timeLabel->setText(tr("Step rest time:"));
+    timeLabel->setGeometry(QRect(10, 100, 141, 20));
+    timeLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+    steptimeEdit = new QDoubleSpinBox(dialog);
+    steptimeEdit->setObjectName(QString::fromUtf8("steptimeEdit"));
+    steptimeEdit->setGeometry(QRect(160, 100, 121, 24));
+    steptimeEdit->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+    steptimeEdit->setDecimals(0);
+    steptimeEdit->setValue(product->mashs.at(product->mashs_row).step_time);
+
+    QLabel *rampLabel = new QLabel(dialog);
+    rampLabel->setObjectName(QString::fromUtf8("rampLabel"));
+    rampLabel->setText(tr("Step ramp time:"));
+    rampLabel->setGeometry(QRect(360, 100, 141, 20));
+    rampLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+    ramptimeEdit = new QDoubleSpinBox(dialog);
+    ramptimeEdit->setObjectName(QString::fromUtf8("ramptimeEdit"));
+    ramptimeEdit->setGeometry(QRect(510, 100, 121, 24));
+    ramptimeEdit->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+    ramptimeEdit->setDecimals(0);
+    ramptimeEdit->setValue(product->mashs.at(product->mashs_row).ramp_time);
+
+    /*
+     * Only used for Infusion steps.
+     */
+    ivolLabel = new QLabel(dialog);
+    ivolLabel->setObjectName(QString::fromUtf8("ivolLabel"));
+    ivolLabel->setText(tr("Infusion volume:"));
+    ivolLabel->setGeometry(QRect(10, 130, 141, 20));
+    ivolLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+    ivolLabel->setVisible(product->mashs.at(product->mashs_row).step_type == 0);
+    stepivolEdit = new QDoubleSpinBox(dialog);
+    stepivolEdit->setObjectName(QString::fromUtf8("stepivolEdit"));
+    stepivolEdit->setGeometry(QRect(160, 130, 121, 24));
+    stepivolEdit->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+    stepivolEdit->setVisible(product->mashs.at(product->mashs_row).step_type == 0);
+    stepivolEdit->setDecimals(1);
+    stepivolEdit->setAccelerated(true);
+    stepivolEdit->setMaximum(100000.0);
+    stepivolEdit->setSingleStep(0.5);
+    stepivolEdit->setValue(product->mashs.at(product->mashs_row).step_infuse_amount);
+
+    itmpLabel = new QLabel(dialog);
+    itmpLabel->setObjectName(QString::fromUtf8("itmpLabel"));
+    itmpLabel->setText(tr("Infusion Temperature:"));
+    itmpLabel->setGeometry(QRect(360, 130, 141, 20));
+    itmpLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+    itmpLabel->setVisible(product->mashs.at(product->mashs_row).step_type == 0);
+    stepitmpEdit = new QDoubleSpinBox(dialog);
+    stepitmpEdit->setObjectName(QString::fromUtf8("stepitmpEdit"));
+    stepitmpEdit->setGeometry(QRect(510, 130, 121, 24));
+    stepitmpEdit->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
+    stepitmpEdit->setVisible(product->mashs.at(product->mashs_row).step_type == 0);
+    stepitmpEdit->setDecimals(1);
+    stepitmpEdit->setReadOnly(true);
+    stepitmpEdit->setButtonSymbols(QAbstractSpinBox::NoButtons);
+    stepitmpEdit->setValue(product->mashs.at(product->mashs_row).step_infuse_temp);
+
+    connect(stepnameEdit, &QLineEdit::textEdited, this, &EditProduct::step_name_changed);
+    connect(typeEdit, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &EditProduct::step_type_changed);
+    connect(steptempEdit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &EditProduct::step_temp_changed);
+    connect(endtempEdit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &EditProduct::end_temp_changed);
+    connect(steptimeEdit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &EditProduct::step_time_changed);
+    connect(ramptimeEdit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &EditProduct::ramp_time_changed);
+    connect(stepivolEdit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &EditProduct::infuse_changed);
+    connect(buttonBox, SIGNAL(rejected()), dialog, SLOT(reject()));
+    connect(buttonBox, SIGNAL(accepted()), dialog, SLOT(accept()));
+
+    dialog->setModal(true);
+    dialog->exec();
+    if (dialog->result() == QDialog::Rejected) {
+        qDebug() << "reject and rollback";
+        product->mashs[product->mashs_row] = backup;
+	/* Rollback water volumes too */
+	double volume = 0;
+    	for (int i = 0; i < product->mashs.size(); i++) {
+            if (product->mashs.at(i).step_type == 0) {
+            	volume += product->mashs.at(i).step_infuse_amount;
+            }
+            product->mashs[i].step_volume = volume;
+    	}
+    	product->w1_amount = volume - product->w2_amount;
+    	product->wg_amount = volume;
+    	ui->w1_volEdit->setValue(product->w1_amount);
+    }
+
+    disconnect(stepnameEdit, nullptr, nullptr, nullptr);
+    disconnect(steptempEdit, nullptr, nullptr, nullptr);
+    disconnect(endtempEdit, nullptr, nullptr, nullptr);
+    disconnect(steptimeEdit, nullptr, nullptr, nullptr);
+    disconnect(ramptimeEdit, nullptr, nullptr, nullptr);
+    disconnect(stepivolEdit, nullptr, nullptr, nullptr);
+    disconnect(buttonBox, nullptr, nullptr, nullptr);
+
+    emit refreshAll();
+}
+
+
+void EditProduct::mash_name_changed(QString name)
+{
+    qDebug() << "mash_name_changed" << name;
+    product->mash_name = name;
+    is_changed();
+}
+
+
+void EditProduct::mash_select_changed(int val)
+{
+    QSqlQuery query;
+    int  i;
+
+    qDebug() << "mash_select_changed" << val;
+
+    const QSignalBlocker blocker1(ui->mash_nameEdit);
+    query.prepare("SELECT name,steps FROM profile_mash ORDER BY name");
+    query.exec();
+    query.first();
+    for (i = 0; i < (val -1); i++) {
+	query.next();
+    }
+
+    product->mash_name = query.value(0).toString();
+    ui->mash_nameEdit->setText(product->mash_name);
+
+    QJsonParseError parseError;
+    const auto& json = query.value(1).toString();
+    if (!json.trimmed().isEmpty()) {
+	const auto& formattedJson = QString("%1").arg(json);
+	QJsonDocument newsteps = QJsonDocument::fromJson(formattedJson.toUtf8(), &parseError);
+
+	if (parseError.error != QJsonParseError::NoError) {
+	    qDebug() << "Parse error: " << parseError.errorString() << "at" << parseError.offset;
+	} else {
+	    /*
+	     * Got the json data in the steps array, replace the product steps.
+	     */
+	    double infuse = 0;
+	    if (product->mashs.size()) {
+	    	infuse = product->mashs.at(0).step_infuse_amount;
+	    	product->mashs.clear();
+	    	ui->mashsTable->clear();
+	    }
+	    if (newsteps.isArray()) {
+		for (i = 0; i < newsteps.array().size(); i++) {
+		    QJsonObject obj = newsteps.array().at(i).toObject();
+		    Mashs m;
+		    m.step_name = obj["step_name"].toString();
+		    if (obj["step_type"].isString())
+			m.step_type = QString(obj["step_type"].toString()).toInt();
+		    else
+		        m.step_type = obj["step_type"].toInt();
+		    m.step_volume = 0;
+		    m.step_infuse_amount = 0;
+		    m.step_infuse_temp = 0;
+		    if (obj["step_temp"].isString())
+			m.step_temp = QString(obj["step_temp"].toString()).toDouble();
+		    else
+		        m.step_temp = obj["step_temp"].toDouble();
+		    if (obj["step_time"].isString())
+			m.step_time = QString(obj["step_time"].toString()).toDouble();
+		    else
+		        m.step_time = obj["step_time"].toDouble();
+		    if (obj["ramp_time"].isString())
+			m.ramp_time = QString(obj["ramp_time"].toString()).toDouble();
+		    else
+		        m.ramp_time = obj["ramp_time"].toDouble();
+		    if (obj["end_temp"].isString())
+			m.end_temp = QString(obj["end_temp"].toString()).toDouble();
+		    else
+		        m.end_temp = obj["end_temp"].toDouble();
+		    m.step_wg_ratio = 0;
+		    product->mashs.append(m);
+		}
+	    }
+	    if (product->mashs.at(0).step_type == 0)
+		product->mashs[0].step_infuse_amount = infuse;	// Restore saved initial infusion
+	}
+    }
+    is_changed();
+    emit refreshAll();
+}
+
+

mercurial