src/EditProductTab6.cpp

Sat, 11 Feb 2023 15:48:02 +0100

author
Michiel Broek <mbroek@mbse.eu>
date
Sat, 11 Feb 2023 15:48:02 +0100
changeset 493
520306773450
parent 487
04c67c9f903c
permissions
-rw-r--r--

Monitor iSpindels: use global variables instead of repeated expensive MySQL calls. Use the yeast temperature ranges for the colors on the thermometer scale. Don't show SVG and ABV if the OG is not yet known. Turn statusfield red if offline. Extra mon_ispindles fields yeast_lo and yeast_hi. Need at least bmsd version 0.3.42. If a websocket message is received that cannot be parsed, show the whole received message.

/**
 * EditProduct.cpp is part of bmsapp.
 *
 * tab 6, yeasts.
 *
 * 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 yeaste 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/>.
 */


bool EditProduct::yeast_sort_test(const Yeasts &D1, const Yeasts &D2)
{
    if (D1.use > D2.use)
	return false;
    if (D1.use < D2.use)
	return true;
    return (D1.amount > D2.amount);
}


bool EditProduct::block_yeast(int stage, int use)
{
    if (stage > PROD_STAGE_PRIMARY && use < YEAST_USE_SECONDARY)
	return true;
    if (stage > PROD_STAGE_SECONDARY && use < YEAST_USE_TERTIARY)
	return true;
    if (stage > PROD_STAGE_TERTIARY && use < YEAST_USE_BOTTLE)
	return true;
    if (stage > PROD_STAGE_PACKAGE)
	return true;
    return false;
}


void EditProduct::refreshYeasts()
{
    QString w;
    QWidget* pWidget;
    QHBoxLayout* pLayout;
    QTableWidgetItem *item;

    std::sort(product->yeasts.begin(), product->yeasts.end(), yeast_sort_test);

    const QStringList labels({tr("Yeast"), tr("Laboratory"), tr("Code"), tr("Type"), tr("Use for"), tr("Min."), tr("Max."),
		              tr("Tol."), tr("Attn."), tr("STA"), tr("Amount"), tr("Stock"), tr("Delete"), tr("Edit") });

    ui->yeastsTable->setColumnCount(14);
    ui->yeastsTable->setColumnWidth(0, 200);	/* Yeast	*/
    ui->yeastsTable->setColumnWidth(1, 115);	/* Laboratory	*/
    ui->yeastsTable->setColumnWidth(2,  80);	/* Code		*/
    ui->yeastsTable->setColumnWidth(3,  75);	/* Type		*/
    ui->yeastsTable->setColumnWidth(4,  75);	/* Usage	*/
    ui->yeastsTable->setColumnWidth(5,  50);	/* Min. 	*/
    ui->yeastsTable->setColumnWidth(6,  50);	/* Max.		*/
    ui->yeastsTable->setColumnWidth(7,  50);	/* Tolerance	*/
    ui->yeastsTable->setColumnWidth(8,  50);	/* Attenuation	*/
    ui->yeastsTable->setColumnWidth(9,  40);	/* STA1 gen	*/
    ui->yeastsTable->setColumnWidth(10, 80);	/* Amount	*/
    ui->yeastsTable->setColumnWidth(11, 80);	/* Stock	*/
    ui->yeastsTable->setColumnWidth(12, 80);	/* Delete	*/
    ui->yeastsTable->setColumnWidth(13, 80);	/* Edit		*/
    ui->yeastsTable->setHorizontalHeaderLabels(labels);
    ui->yeastsTable->verticalHeader()->hide();
    ui->yeastsTable->setRowCount(product->yeasts.size());

    for (int i = 0; i < product->yeasts.size(); i++) {

	ui->yeastsTable->setItem(i, 0, new QTableWidgetItem(product->yeasts.at(i).name));
	ui->yeastsTable->setItem(i, 1, new QTableWidgetItem(product->yeasts.at(i).laboratory));
	ui->yeastsTable->setItem(i, 2, new QTableWidgetItem(product->yeasts.at(i).product_id));

	item = new QTableWidgetItem(QCoreApplication::translate("YeastForm", g_yeast_forms[product->yeasts.at(i).form]));
        item->setTextAlignment(Qt::AlignCenter|Qt::AlignVCenter);
        ui->yeastsTable->setItem(i, 3, item);

        item = new QTableWidgetItem(QCoreApplication::translate("YeastUse", g_yeast_use[product->yeasts.at(i).use]));
        item->setTextAlignment(Qt::AlignCenter|Qt::AlignVCenter);
        ui->yeastsTable->setItem(i, 4, item);

	item = new QTableWidgetItem(QString("%1°C").arg(product->yeasts.at(i).min_temperature, 2, 'f', 1, '0'));
	item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
        ui->yeastsTable->setItem(i, 5, item);

	item = new QTableWidgetItem(QString("%1°C").arg(product->yeasts.at(i).max_temperature, 2, 'f', 1, '0'));
        item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
        ui->yeastsTable->setItem(i, 6, item);

	item = new QTableWidgetItem(QString("%1%").arg(product->yeasts.at(i).tolerance, 2, 'f', 1, '0'));
        item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
        ui->yeastsTable->setItem(i, 7, item);

	item = new QTableWidgetItem(QString("%1%").arg(product->yeasts.at(i).attenuation, 2, 'f', 1, '0'));
        item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
        ui->yeastsTable->setItem(i, 8, item);

	if (product->yeasts.at(i).use != YEAST_USE_BOTTLE && product->yeasts.at(i).sta1) {
	    QWidget *pWidget = new QWidget();
            QLabel *label = new QLabel;
            label->setPixmap(QPixmap(":icons/silk/tick.png"));
            QHBoxLayout *pLayout = new QHBoxLayout(pWidget);
            pLayout->addWidget(label);
            pLayout->setAlignment(Qt::AlignCenter);
            pLayout->setContentsMargins(0, 0, 0, 0);
            pWidget->setLayout(pLayout);
            ui->yeastsTable->setCellWidget(i, 9, pWidget);
	} else {
	    ui->yeastsTable->removeCellWidget(i, 9);
	}


	if (product->yeasts.at(i).form == YEAST_FORMS_LIQUID)
            item = new QTableWidgetItem(QString("%1 pack").arg(product->yeasts.at(i).amount, 1, 'f', 0, '0'));
	else if (product->yeasts.at(i).form == YEAST_FORMS_DRY || product->yeasts.at(i).form == YEAST_FORMS_DRIED)
	    item = new QTableWidgetItem(QString("%1 gr").arg(product->yeasts.at(i).amount * 1000.0, 3, 'f', 2, '0'));
	else
	    item = new QTableWidgetItem(QString("%1 ml").arg(product->yeasts.at(i).amount * 1000.0, 3, 'f', 2, '0'));
        item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
        ui->yeastsTable->setItem(i, 10, item);

	if (block_yeast(product->stage, product->yeasts.at(i).use)) {
	    item = new QTableWidgetItem(QString(""));
	} else {
	    if (product->yeasts.at(i).form == YEAST_FORMS_LIQUID)
            	item = new QTableWidgetItem(QString("%1 pack").arg(product->yeasts.at(i).inventory, 1, 'f', 0, '0'));
            else if (product->yeasts.at(i).form == YEAST_FORMS_DRY || product->yeasts.at(i).form == YEAST_FORMS_DRIED)
            	item = new QTableWidgetItem(QString("%1 gr").arg(product->yeasts.at(i).inventory * 1000.0, 3, 'f', 2, '0'));
            else
            	item = new QTableWidgetItem(QString("%1 ml").arg(product->yeasts.at(i).inventory * 1000.0, 3, 'f', 2, '0'));
            item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
	    if (product->yeasts.at(i).inventory < product->yeasts.at(i).amount)
	    	item->setForeground(QBrush(QColor(Qt::red)));
	}
        ui->yeastsTable->setItem(i, 11, item);

	if (block_yeast(product->stage, product->yeasts.at(i).use)) {
	    ui->yeastsTable->removeCellWidget(i, 12);     /* to remove the unneeded button */
            item = new QTableWidgetItem("");
            item->setToolTip(tr("Yeast already used"));
            ui->yeastsTable->setItem(i, 12, item);
            ui->yeastsTable->removeCellWidget(i, 13);
            item = new QTableWidgetItem("");
            item->setToolTip(tr("Yeast already used"));
            ui->yeastsTable->setItem(i, 13, item);
	} else {
	    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(deleteYeastRow_clicked()));
            pLayout = new QHBoxLayout(pWidget);
            pLayout->addWidget(btn_dele);
            pLayout->setContentsMargins(5, 0, 5, 0);
            pWidget->setLayout(pLayout);
            ui->yeastsTable->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(editYeastRow_clicked()));
            pLayout = new QHBoxLayout(pWidget);
            pLayout->addWidget(btn_edit);
            pLayout->setContentsMargins(5, 0, 5, 0);
            pWidget->setLayout(pLayout);
            ui->yeastsTable->setCellWidget(i, 13, pWidget);
	}
    }
}


void EditProduct::initYeast()
{
    ui->est_og4Edit->setValue(product->est_og);
    ui->est_fg3Edit->setValue(product->est_fg);
    ui->est_abv2Edit->setValue(product->est_abv);
    ui->productionEdit->setDate(product->yeast_prod_date);
    ui->conditionShow->setValue(product->starter_viability);
    ui->startersgEdit->setValue(product->starter_sg);
    ui->pitchrateEdit->setValue(product->yeast_pitchrate);

    ui->yeastsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);

    for (int i = 0; i < 3; i++) {
	ui->stmethodEdit->addItem(QCoreApplication::translate("YeastStarter", g_yeast_starter[i]));
    }
    ui->stmethodEdit->setCurrentIndex(product->starter_type);
}


/*
 * Calculate the needed yeast for this batch.
 */
void EditProduct::calcYeast()
{
    double sg = product->brew_fermenter_sg;
    double use_cells;
    double needed = 0;
    double initcells = 0;
    bool maybe_starter = false;

    qDebug() << "calcYeast()";
    ui->yeastProcedure->setCurrentIndex(0);

    if (sg <= 1.0001 && product->fg > 1.000)
	sg = product->fg;
    else if (sg <= 1.0001)
	sg = product->est_og;
    double plato = Utils::sg_to_plato(sg);

    double volume = product->brew_fermenter_volume;
    if (volume > 0) {
	if (product->brew_fermenter_extrawater > 0)
	    volume += product->brew_fermenter_extrawater;
    } else {
	volume = product->batch_size - product->eq_trub_loss;
    }

    if (product->yeasts.size() == 0)
	return;		// No yeast in product.

    calcViability();
    double dry_amount = 0;
    for (int i = 0; i < product->yeasts.size(); i++) {
	if (product->yeasts.at(i).use == YEAST_USE_PRIMARY && product->yeasts.at(i).form == YEAST_FORMS_DRY) {
	    dry_amount += product->yeasts.at(i).amount;
	}
    }

    for (int i = 0; i < product->yeasts.size(); i++) {
	if (product->yeasts.at(i).use == YEAST_USE_PRIMARY) {		// Primary
	    if (product->yeasts.at(i).form == YEAST_FORMS_DRY) {
		/*
		 * Dry yeast, build the formule with the yeast parameters.
		 * Based on https://www.lallemandbrewing.com/en/canada/brewers-corner/brewing-tools/pitching-rate-calculator/
		 */
		ui->yeastProcedure->setCurrentIndex(2);
		ui->lo_gr_hlEdit->setValue(product->yeasts.at(i).gr_hl_lo);
		ui->hi_gr_hlEdit->setValue(product->yeasts.at(i).gr_hl_hi);
		ui->lo_sgEdit->setValue(product->yeasts.at(i).sg_lo);
		ui->hi_sgEdit->setValue(product->yeasts.at(i).sg_hi);
		double og = product->yeasts.at(i).sg_lo;
		double f1 = product->yeasts.at(i).gr_hl_lo / 100.0;
		double f2 = round(f1 / 5 * 1000000.0) / 1000000.0;
     		double multiplier = (sg <= og) ? f1 : (f1 + f2 * (sg - og) / 0.008);
		qDebug() << "  sg:" << sg << "og:" << og << "f1:" << f1 << "f2:" << f2 << "multiplier:" << multiplier;
     		double yeast_grams = round(volume * multiplier * 100.0) / product->starter_viability;
     		double yeast_gr_hl = round((yeast_grams / (volume * 0.01)) * 100.0) / 100.0;
		double pitch_gr_hl = round(((dry_amount * 1000.0) / (volume * 0.01)) * 100.0) / 100.0;
		ui->dry_needShow->setValue(yeast_grams);
		ui->dry_pitchrateShow->setValue(yeast_gr_hl);
		ui->pitch_grShow->setValue(pitch_gr_hl);
		ui->pitch_grShow->setStyleSheet((pitch_gr_hl < yeast_gr_hl) ? "background-color: red":"");

#ifdef DEBUG_YEAST
     		qDebug() << "  Need" << yeast_grams << "grams, gr/hl:" << yeast_gr_hl << "pitch:" << pitch_gr_hl;
#endif
		calcBU();
		return;
	    } else {
		/*
		 * Liquid, slant, culture etc.
		 * pitchrate see https://www.brewersfriend.com/yeast-pitch-rate-and-starter-calculator/
		 * and http://braukaiser.com/blog/blog/2012/11/03/estimating-yeast-growth/
		 */
		ui->yeastProcedure->setCurrentIndex(1);
		if (product->yeast_pitchrate == 0) {
		    /*
		     * No pitchrate yet, do a educated guess ..
		     */
		    if (product->yeasts.at(i).type == YEAST_TYPES_LAGER) {
		    	product->yeast_pitchrate = 1.5;
		    	if (sg > 1.060)
			    product->yeast_pitchrate = 2.0;
		    } else if (product->yeasts.at(i).type == YEAST_TYPES_KVEIK) {	// Real Kveik
		    	product->yeast_pitchrate = 0.075;
		    } else {
		    	product->yeast_pitchrate = 0.75;
		    	if (sg > 1.060)
			    product->yeast_pitchrate = 1.0;
		    }
		    is_changed();
		    ui->pitchrateEdit->setValue(product->yeast_pitchrate);
#ifdef DEBUG_YEAST
		    qDebug() << "  Guessed pitchrate" << product->yeast_pitchrate;
#endif
		}

		double cells = product->yeasts.at(i).cells;
		if (product->yeasts.at(i).yp_cells > 0) {	// Use from yeastpack if set
		    cells = product->yeasts.at(i).yp_cells;
		}
		initcells = (cells / 1000000) * product->yeasts.at(i).amount * (product->starter_viability / 100.0);
		if (product->yeasts.at(i).form == YEAST_FORMS_LIQUID)
		    initcells = (cells / 1000000000) * product->yeasts.at(i).amount * (product->starter_viability / 100.0);

		needed = round(product->yeast_pitchrate * volume * plato * 10.0) / 10.0;
		ui->neededShow->setValue(needed);
		if ((0.9 * needed) > initcells) {	// Allow 90% underpitch without a starter
		    maybe_starter = true;
		}

#ifdef DEBUG_YEAST
		qDebug() << "  Pitchrate:" << product->yeast_pitchrate << "needed:" << 0.9 * needed << "/" << needed << "initcells:" << initcells << "starter" << maybe_starter;
#endif
	    }
	    break;
	}
    }

    if (maybe_starter != product->starter_enable) {
	if (product->starter_enable && !maybe_starter) {
#ifdef DEBUG_YEAST
	    qDebug() << "  Clear obsolete starter";
#endif
	    for (int i = 0; i < 4; i++) {
		product->prop_volume[i] = 0;
		product->prop_type[i] = 0;
	    }
	}
	product->starter_enable = maybe_starter;
#ifdef DEBUG_YEAST
	qDebug() << "  Set starter enable" << maybe_starter;
#endif
	is_changed();
    }

    if (product->starter_enable) {
#ifdef DEBUG_YEAST
	qDebug() << "  Starter calculate..";
#endif

	const QStringList labels({tr("Method"), tr("Volume"), tr("Inj. factor"), tr("New cells"), tr("Total cells"), tr("Grow factor"), "" });
	ui->starterTable->show();
	ui->restartLabel->show();
	ui->restartButton->show();
	ui->starterTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
	ui->starterTable->clear();
	ui->starterTable->setColumnCount(7);
	ui->starterTable->setRowCount(0);
	ui->starterTable->setColumnWidth(0, 130);	/* Method	*/
	ui->starterTable->setColumnWidth(1,  90);	/* Volume	*/
	ui->starterTable->setColumnWidth(2,  90);	/* Inj. factor	*/
	ui->starterTable->setColumnWidth(3,  90);	/* New cells	*/
	ui->starterTable->setColumnWidth(4,  90);	/* Total cells	*/
	ui->starterTable->setColumnWidth(5,  90);	/* Grow factor	*/
	ui->starterTable->setColumnWidth(6,  30);	/* Edit button	*/
	ui->starterTable->setHorizontalHeaderLabels(labels);
	calcSteps(product->starter_type, initcells, needed);
    } else {
	ui->starterTable->hide();
	ui->restartLabel->hide();
	ui->restartButton->hide();
    }
    calcBU();
}


/*
 * http://braukaiser.com/blog/blog/2012/11/03/estimating-yeast-growth/
 *
 * stype: 0=stirred, 1=shaken, 2=simple
 * totcells: initial cells
 * egrams: gram extract
 */
double EditProduct::getGrowthRate(int stype, double totcells, double egrams)
{
    /* Cells per grams extract (B/g) */
    double cpe = totcells / egrams;

    if (cpe > 3.5)
        return 0;       // no growth
    if (stype == STARTERS_SIMPLE)
        return 0.4;     // simple starter
    if (stype == STARTERS_SHAKEN)
        return 0.62;    // shaken starter
    if (cpe <= 1.4)     // stirred starter
        return 1.4;
    return 2.33 - (.67 * cpe);
};


StepResult EditProduct::calcStep(double svol, int stype, double start)
{
    StepResult res;
    double gperpoint = 2.72715;	//number of grams of extract per point of starter gravity per liter
    double irate = round(start / svol * 10000.0) / 10.0;
    double egrams = (product->starter_sg - 1) * svol * gperpoint;
    double grate = getGrowthRate(stype, start, egrams);
    double ncells = round(egrams * grate * 10.0) / 10.0;
    double totcells = ncells + start;

    res.svol = svol;
    res.irate = irate;
    res.ncells = ncells;
    res.totcells = totcells;
    res.growf = round((ncells / start) * 100.0) / 100.0;
#ifdef DEBUG_YEAST
    qDebug() << "    calcStep(" << svol << "," << stype << "," << start << ") irate" << irate
	     << "ncells" << res.ncells << "totcells" << res.totcells << "growf" << res.growf;
#endif
    return res;
}


/*
 * Calculate all starter steps.
 * stype: final starter type: 0 = stirred, 1 = shaked, 2 = simple.
 * start: initial cells in billions
 * needed: needed cells in billions
 *
 * result: all values updated.
 */
void EditProduct::calcSteps(int stype, double start, double needed)
{
    int	i, step, svol;
    int	lasti = 0;
    /* Erlenmeyer sizes */
    const int uvols[] { 20, 40, 60, 80, 100, 150, 200, 250, 375, 500, 625, 750, 875, 1000, 1250, 1500, 2000, 2500, 3000, 4000, 5000 };
    int mvols = sizeof(uvols);
    StepResult result;
    QTableWidgetItem *item;
    QWidget* pWidget;
    QHBoxLayout* pLayout;
    double tcells = start;
    QIcon iconT;

    iconT.addFile(QString::fromUtf8(":/icons/silk/pencil.png"), QSize(), QIcon::Normal, QIcon::Off);

    if ((product->prop_volume[0] + product->prop_volume[1] + product->prop_volume[2] + product->prop_volume[3]) == 0) {
	/*
	 * Auto calculate the starter.
	 */
#ifdef DEBUG_YEAST
	qDebug() << "  calcSteps() auto";
#endif

	if (start > needed)
	    return;

	for (step = 1; step < 5; step++) {
	    for (i = lasti; i <= mvols; i++) {
		lasti = i;
		svol = uvols[lasti];
		result = calcStep(svol, stype, tcells);
		if (result.irate < 25) {
		    // inocculation rate too low, backup one step and break out.
		    lasti = i - 1;
		    svol = uvols[lasti];
		    result = calcStep(svol, stype, tcells);
		    break;
		}
		if (result.totcells > needed || i == mvols) {	// hit the target or loops done
		    break;
		}
	    }
	    ui->starterTable->setRowCount(step);
	    item = new QTableWidgetItem(QCoreApplication::translate("YeastStarter", g_yeast_starter[stype]));
	    item->setTextAlignment(Qt::AlignCenter|Qt::AlignVCenter);
	    ui->starterTable->setItem(step -1, 0, item);

	    item = new QTableWidgetItem(QString("%1").arg(result.svol / 1000.0, 4, 'f', 3, '0'));	// To liters
	    item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
	    ui->starterTable->setItem(step -1, 1, item);

	    item = new QTableWidgetItem(QString("%1").arg(result.irate, 2, 'f', 1, '0'));
	    item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
	    if ((result.irate < 25) || (result.irate > 100))
		item->setForeground(QBrush(QColor(Qt::red)));
            ui->starterTable->setItem(step -1, 2, item);

	    item = new QTableWidgetItem(QString("%1").arg(result.ncells, 2, 'f', 1, '0'));
            item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
            ui->starterTable->setItem(step -1, 3, item);

	    item = new QTableWidgetItem(QString("%1").arg(result.totcells, 2, 'f', 1, '0'));
            item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
	    item->setForeground(QBrush(QColor((result.totcells > needed) ? Qt::green:Qt::red)));
            ui->starterTable->setItem(step -1, 4, item);

	    item = new QTableWidgetItem(QString("%1").arg(result.growf, 3, 'f', 2, '0'));
            item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
	    if (result.growf < 1)
		item->setForeground(QBrush(QColor(Qt::red)));
	    if ((stype > 0) && (result.growf > 3))
		item->setForeground(QBrush(QColor(Qt::red)));
            ui->starterTable->setItem(step -1, 5, item);

	    pWidget = new QWidget();
            QToolButton* btn_edit = new QToolButton();
            btn_edit->setObjectName(QString("%1").arg(step - 1));  /* Send row with the button */
	    btn_edit->setIcon(iconT);
            connect(btn_edit, SIGNAL(clicked()), this, SLOT(yeast_starter_edit_clicked()));
            pLayout = new QHBoxLayout(pWidget);
            pLayout->addWidget(btn_edit);
            pLayout->setContentsMargins(5, 0, 5, 0);
            pWidget->setLayout(pLayout);
            ui->starterTable->setCellWidget(step -1, 6, pWidget);

	    product->prop_type[step -1] = product->starter_type;
	    product->prop_volume[step -1] = result.svol / 1000.0;
#ifdef DEBUG_YEAST
	    qDebug() << "    step" << step << "type" << product->prop_type[step -1] << "vol" << product->prop_volume[step -1];
#endif

	    tcells = result.totcells;
	    if (result.totcells > needed)	// Hit the target
		return;
	}

    } else {
	/*
	 * Recalculate the starter.
	 */
#ifdef DEBUG_YEAST
	qDebug() << "  calcSteps() recalculate";
#endif

	for (step = 0; step < 4; step++) {
	    if (product->prop_volume[step] > 0) {
	    	result = calcStep(product->prop_volume[step] * 1000, product->prop_type[step], tcells);
	    	ui->starterTable->setRowCount(step + 1);
	    	item = new QTableWidgetItem(QCoreApplication::translate("YeastStarter", g_yeast_starter[product->prop_type[step]]));
	    	item->setTextAlignment(Qt::AlignCenter|Qt::AlignVCenter);
	    	ui->starterTable->setItem(step, 0, item);

            	item = new QTableWidgetItem(QString("%1").arg(result.svol / 1000.0, 4, 'f', 3, '0'));       // To liters
            	item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
            	ui->starterTable->setItem(step, 1, item);

            	item = new QTableWidgetItem(QString("%1").arg(result.irate, 2, 'f', 1, '0'));
            	item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
		if ((result.irate < 25) || (result.irate > 100))
                    item->setForeground(QBrush(QColor(Qt::red)));
            	ui->starterTable->setItem(step, 2, item);

            	item = new QTableWidgetItem(QString("%1").arg(result.ncells, 2, 'f', 1, '0'));
            	item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
            	ui->starterTable->setItem(step, 3, item);

            	item = new QTableWidgetItem(QString("%1").arg(result.totcells, 2, 'f', 1, '0'));
            	item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
		item->setForeground(QBrush(QColor((result.totcells > needed) ? Qt::green:Qt::red)));
            	ui->starterTable->setItem(step, 4, item);

            	item = new QTableWidgetItem(QString("%1").arg(result.growf, 3, 'f', 2, '0'));
            	item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
		if (result.growf < 1)
                    item->setForeground(QBrush(QColor(Qt::red)));
            	if ((stype > 0) && (result.growf > 3))
                    item->setForeground(QBrush(QColor(Qt::red)));
            	ui->starterTable->setItem(step, 5, item);

	    	pWidget = new QWidget();
            	QToolButton* btn_edit = new QToolButton();
            	btn_edit->setObjectName(QString("%1").arg(step));  /* Send row with the button */
            	btn_edit->setIcon(iconT);
            	connect(btn_edit, SIGNAL(clicked()), this, SLOT(yeast_starter_edit_clicked()));
            	pLayout = new QHBoxLayout(pWidget);
            	pLayout->addWidget(btn_edit);
            	pLayout->setContentsMargins(5, 0, 5, 0);
            	pWidget->setLayout(pLayout);
            	ui->starterTable->setCellWidget(step, 6, pWidget);

	    	tcells = result.totcells;
	    	if (result.totcells > needed) {	// Hit the target
		    for (int i = step + 1; i < 4; i++) {
		    	product->prop_volume[i] = 0;
		    	product->prop_type[i] = 0;
		    }
                    return;
	    	} else if ((step < 3) && (product->prop_volume[step + 1] == 0)) {	// Extra step needed, start with the same size.
		    product->prop_volume[step + 1] = product->prop_volume[step];
		    product->prop_type[step + 1] = product->prop_type[step];
	    	}
#ifdef DEBUG_YEAST
		qDebug() << "    step" << step << "type" << product->prop_type[step] << "vol" << product->prop_volume[step];
#endif
	    }
	}
    }
}


void EditProduct::calcViability()
{
    double vpm = 1.00;
    double max = 100;

    for (int i = 0; i < product->yeasts.size(); i++) {
	if (product->yeasts.at(i).use == YEAST_USE_PRIMARY) {
	    if (! product->yeasts.at(i).yp_uuid.isNull() && (product->yeasts.at(i).yp_uuid.length() == 36)) {
		vpm = product->yeasts.at(i).yp_viability;
		max = product->yeasts.at(i).yp_max;
	    } else if (product->yeasts.at(i).form == YEAST_FORMS_LIQUID) {	// Fallback to hardcoded values.
		vpm = 0.80;
		max = 97;
		if (product->yeasts.at(i).laboratory == "White Labs") {	// PurePitch
		    vpm = 0.9648;	// Purepitch
		    max = 100;
		}
	    } else if (product->yeasts.at(i).form == YEAST_FORMS_DRY) {
		vpm = 0.998;
		max = 100;
	    } else if (product->yeasts.at(i).form == YEAST_FORMS_DRIED) {	// dried kveik
		vpm = 0.92;
		max = 100;
	    } else {	// Slant, Culture, Frozen, Bottle
		vpm = 0.99;
		max = 97;
	    }
	}
    }
#ifdef DEBUG_YEAST
    qDebug() << "calcViability vpm:" << vpm << "max:" << max;
#endif

    double base = max;

    /*
     * Calculate time days before today. If the date is cleared, 
     * the result is 0 days. Dates in the future are ignored.
     */
    int timeDiff = product->yeast_prod_date.daysTo(QDate::currentDate());
    if (timeDiff < 0)
	timeDiff == 0;

    double degrade = 1 - ((1 - vpm) / 30.41);	// viability degradation per day.
    for (int i = 0; i < timeDiff; i++) {
	base = base * degrade;
    }
    if (base > max)
	base = max;
    product->starter_viability = round(base * 10) / 10;
    ui->conditionShow->setValue(product->starter_viability);
#ifdef DEBUG_YEAST
    qDebug() << "  Age" << timeDiff << "degrade" << degrade << "viability" << product->starter_viability;
#endif
}


void EditProduct::yeast_prod_date_changed(QDate val)
{
    product->yeast_prod_date = ui->productionEdit->nullDate();
    calcViability();
    calcYeast();
    is_changed();
}


void EditProduct::yeast_prod_date_clear()
{
    product->yeast_prod_date = QDate();
    ui->productionEdit->setDate(QDate());
}


void EditProduct::yeast_prod_date_today()
{
    product->yeast_prod_date = QDate::currentDate();
    ui->productionEdit->setDate(QDate::currentDate());
}


void EditProduct::yeast_method_changed(int val)
{
#ifdef DEBUG_YEAST
    qDebug() << "yeast_method_changed" << val;
#endif
    product->starter_type = val;
    calcYeast();
    is_changed();
}


void EditProduct::yeast_starter_sg_changed(double val)
{
#ifdef DEBUG_YEAST
    qDebug() << "yeast_starter_sg_changed" << val;
#endif
    product->starter_sg = val;
    calcYeast();
    is_changed();
}


void EditProduct::pitchindex_changed(int val)
{
#ifdef DEBUG_YEAST
    qDebug() << "pitchindex_changed" << val;
#endif
    product->pitch_pitchindex = val;
}


void EditProduct::pitchrate_changed(double val)
{
#ifdef DEBUG_YEAST
    qDebug() << "pitchrate_changed" << val;
#endif
    product->yeast_pitchrate = val;
    calcYeast();
    is_changed();
}


void EditProduct::yeast_pitchrate_button_clicked()
{
#ifdef DEBUG_YEAST
    qDebug() << "yeast_pitchrate_button_clicked";
#endif
    product->pitch_pitchindex = 1;

    QDialog* dialog = new QDialog(this);
    dialog->resize(420, 110);
    dialog->setWindowTitle(tr("BMSapp - Pitchrate"));

    QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog);
    buttonBox->setObjectName(QString::fromUtf8("buttonBox"));
    buttonBox->setGeometry(QRect(30, 60, 360, 32));
    buttonBox->setLayoutDirection(Qt::LeftToRight);
    buttonBox->setOrientation(Qt::Horizontal);
    buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok);
    buttonBox->setCenterButtons(true);

    QLabel *typeLabel = new QLabel(dialog);
    typeLabel->setObjectName(QString::fromUtf8("typeLabel"));
    typeLabel->setText(tr("Beer pitch type:"));
    typeLabel->setGeometry(QRect(10, 20, 195, 20));
    typeLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);

    QComboBox *typeEdit = new QComboBox(dialog);
    typeEdit->setObjectName(QString::fromUtf8("typeEdit"));
    typeEdit->setGeometry(QRect(215, 20, 185, 23));
    typeEdit->setIconSize(QSize(0, 0));
    typeEdit->addItem(tr("0.075 Real Kveik"));
    typeEdit->addItem(tr("0.75  Ale, upto 1.060"));
    typeEdit->addItem(tr("1.0   Ale, above 1.060"));
    typeEdit->addItem(tr("1.5   Lager, upto 1.060"));
    typeEdit->addItem(tr("2.0   Lager, above 1.060"));
    typeEdit->setCurrentIndex(product->pitch_pitchindex);

    connect(typeEdit, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &EditProduct::pitchindex_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) {
	switch (product->pitch_pitchindex) {
	    case 0:	product->yeast_pitchrate = 0.075; break;
	    case 1:	product->yeast_pitchrate = 0.75;  break;
	    case 2:	product->yeast_pitchrate = 1.0;   break;
	    case 3:	product->yeast_pitchrate = 1.5;   break;
	    case 4:	product->yeast_pitchrate = 2.0;   break;
	}
	ui->pitchrateEdit->setValue(product->yeast_pitchrate);	/* Will automatic call calcYeast() and is_changed() */
    }

    disconnect(typeEdit, nullptr, nullptr, nullptr);
    disconnect(buttonBox, nullptr, nullptr, nullptr);
}


void EditProduct::yeast_retry_button_clicked()
{
#ifdef DEBUG_YEAST
    qDebug() << "yeast_retry_button_clicked";
#endif

    int rc = QMessageBox::warning(this, tr("Retry starter"), tr("Retry to automatic create starter steps"),
                    QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
    if (rc == QMessageBox::No)
        return;

    for (int i = 0; i < 4; i++) {
	product->prop_volume[i] = 0;
	product->prop_type[i] = product->starter_type;
    }
    calcYeast();
    is_changed();
}


void EditProduct::yeast_starter_edit_clicked()
{
    QToolButton *pb = qobject_cast<QToolButton *>(QObject::sender());
    int row = pb->objectName().toInt();
#ifdef DEBUG_YEAST
    qDebug() << "yeast_starter_edit_clicked" << row;
#endif

    QDialog* dialog = new QDialog(this);
    dialog->resize(338, 140);
    QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog);
    buttonBox->setObjectName(QString::fromUtf8("buttonBox"));
    buttonBox->setGeometry(QRect(30, 90, 271, 32));
    buttonBox->setLayoutDirection(Qt::LeftToRight);
    buttonBox->setOrientation(Qt::Horizontal);
    buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok);
    buttonBox->setCenterButtons(true);

    QLabel *typeLabel = new QLabel(dialog);
    typeLabel->setObjectName(QString::fromUtf8("typeLabel"));
    typeLabel->setText(tr("Start step type:"));
    typeLabel->setGeometry(QRect(10, 10, 141, 20));
    typeLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
    QLabel *volLabel = new QLabel(dialog);
    volLabel->setObjectName(QString::fromUtf8("volLabel"));
    volLabel->setText(tr("Starter step volume:"));
    volLabel->setGeometry(QRect(10, 40, 141, 20));
    volLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);

    QComboBox *typeEdit = new QComboBox(dialog);
    typeEdit->setObjectName(QString::fromUtf8("typeEdit"));
    typeEdit->setGeometry(QRect(160, 10, 121, 23));
    typeEdit->addItem(tr("Stirred"));
    typeEdit->addItem(tr("Shaken"));
    typeEdit->addItem(tr("Simple"));
    typeEdit->setCurrentIndex(product->prop_type[row]);

    QDoubleSpinBox *volEdit = new QDoubleSpinBox(dialog);
    volEdit->setObjectName(QString::fromUtf8("volEdit"));
    volEdit->setGeometry(QRect(160, 40, 121, 24));
    volEdit->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
    volEdit->setAccelerated(true);
    volEdit->setDecimals(3);
    volEdit->setSingleStep(0.01);
    volEdit->setValue(product->prop_volume[row]);
    volEdit->setMaximum(5);

    connect(buttonBox, SIGNAL(rejected()), dialog, SLOT(reject()));
    connect(buttonBox, SIGNAL(accepted()), dialog, SLOT(accept()));

    dialog->setModal(true);
    dialog->exec();
    if (dialog->result() != QDialog::Rejected) {
	product->prop_type[row] = typeEdit->currentIndex();
	product->prop_volume[row] = volEdit->value();
        calcYeast();
	is_changed();
    }

    disconnect(buttonBox, nullptr, nullptr, nullptr);
}


void EditProduct::addYeastRow_clicked()
{
    Yeasts newy;

#ifdef DEBUG_YEAST
    qDebug() << "Add yeast row";
#endif

    for (int i = 0; i < product->yeasts.size(); i++) {
        if (product->yeasts.at(i).amount == 0)
            return;     // Add only one at a time.
    }

    newy.name = "Select one";
    newy.laboratory = "";
    newy.product_id = "";
    newy.amount = 0;
    newy.type = YEAST_TYPES_ALE;
    newy.form = YEAST_FORMS_LIQUID;
    newy.min_temperature = 0;
    newy.max_temperature = 0;
    newy.flocculation = 0;
    newy.attenuation = 0;
    newy.cells = 0;
    newy.tolerance = 0;
    newy.inventory = 0;
    newy.use = YEAST_USE_PRIMARY;
    newy.sta1 = false;
    newy.bacteria = false;
    newy.harvest_top = false;
    newy.harvest_time = 0;
    newy.pitch_temperature = 0;
    newy.pofpos = false;
    newy.zymocide = 0;
    newy.gr_hl_lo = 0;
    newy.sg_lo = 0;
    newy.gr_hl_hi = 0;
    newy.sg_hi = 0;
    newy.cost = 0;

    product->yeasts.append(newy);
    emit refreshAll();
}


void EditProduct::deleteYeastRow_clicked()
{
    if (product->locked || product->yeasts.size() < 1)
	return;

    QPushButton *pb = qobject_cast<QPushButton *>(QObject::sender());
    int row = pb->objectName().toInt();
#ifdef DEBUG_YEAST
    qDebug() << "Delete yeast row" << row << product->yeasts.size();
#endif

    int rc = QMessageBox::warning(this, tr("Delete yeast"), tr("Delete %1").arg(product->yeasts.at(row).name),
                    QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
    if (rc == QMessageBox::No)
        return;

    product->yeasts.removeAt(row);
    bool primary = false;	/* Check if any primary yeast is left */
    for (int i = 0; i < product->yeasts.size(); i++) {
	if (product->yeasts.at(i).use == YEAST_USE_PRIMARY)
	    primary = true;
    }
    if (! primary) {
#ifdef DEBUG_YEAST
	qDebug() << "  Clear starter (if any)";
#endif
	for (int i = 0; i < 4; i++)
	    product->prop_volume[i] = 0;
    }
    is_changed();
    emit refreshAll();
}


void EditProduct::yeast_amount_changed(double val)
{
    QTableWidgetItem *item;

#ifdef DEBUG_YEAST
    qDebug() << "yeast_amount_changed()" << product->yeasts_row << val;
#endif

    if (product->yeasts.at(product->yeasts_row).form == YEAST_FORMS_LIQUID) {
	product->yeasts[product->yeasts_row].amount = val;
        item = new QTableWidgetItem(QString("%1 pack").arg(val, 1, 'f', 0, '0'));
    } else if ((product->yeasts.at(product->yeasts_row).form == YEAST_FORMS_DRY) ||
	       (product->yeasts.at(product->yeasts_row).form == YEAST_FORMS_DRIED)) {
	product->yeasts[product->yeasts_row].amount = val / 1000.0;
        item = new QTableWidgetItem(QString("%1 gr").arg(val, 3, 'f', 2, '0'));
    } else {
        product->yeasts[product->yeasts_row].amount = val / 1000.0;
        item = new QTableWidgetItem(QString("%1 ml").arg(val, 3, 'f', 2, '0'));
    }
    item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
    ui->yeastsTable->setItem(product->yeasts_row, 10, item);

    calcYeast();
    is_changed();
}


void EditProduct::yeast_load_packages(QString laboratory, int form)
{
    QSqlQuery query;

    /*
     * Clear package and rebuild package select table.
     */
    this->ypackageEdit->setCurrentIndex(-1);
    this->ypackageEdit->clear();
    this->ypackageEdit->addItem("");    // Start with empty value
    query.prepare("SELECT * FROM inventory_yeastpack WHERE laboratory=:laboratory AND form=:form AND valid='1' ORDER BY laboratory,package");
    query.bindValue(":laboratory", laboratory);
    query.bindValue(":form", form);
//    qDebug() << "  search" << laboratory << form;
    query.exec();
    while (query.next()) {
        this->ypackageEdit->addItem(query.value("laboratory").toString()+" - "+query.value("package").toString());
//        qDebug() << "  add package" << query.value("laboratory").toString() << query.value("package").toString();
    }
}


void EditProduct::yeast_package_changed(int val)
{
    QSqlQuery query;
    int index = 0;

#ifdef DEBUG_YEAST
    qDebug() << "yeast_package_changed()" << product->yeasts_row << val;
#endif

    /*
     * Scan the packages for result number 'val'
     */
    query.prepare("SELECT * FROM inventory_yeastpack WHERE laboratory=:laboratory AND form=:form AND valid='1' ORDER BY laboratory,package");
    query.bindValue(":laboratory", product->yeasts.at(product->yeasts_row).laboratory);
    query.bindValue(":form", product->yeasts.at(product->yeasts_row).form);
//    qDebug() << "  search" << product->yeasts.at(product->yeasts_row).laboratory << product->yeasts.at(product->yeasts_row).form;
    query.exec();
    while (query.next()) {
	index++;
	if (index == val) {
//	    qDebug() << "  result" << index << query.value("package").toString();
	    product->yeasts[product->yeasts_row].yp_uuid = query.value("uuid").toString();
	    product->yeasts[product->yeasts_row].yp_package = query.value("package").toString();
	    product->yeasts[product->yeasts_row].yp_cells = query.value("cells").toDouble();
	    product->yeasts[product->yeasts_row].yp_viability = query.value("viability").toDouble();
	    product->yeasts[product->yeasts_row].yp_max = query.value("max").toInt();
	    product->yeasts[product->yeasts_row].yp_size = query.value("size").toDouble();
	    is_changed();
	    return;
	}
    }
    if (! product->yeasts[product->yeasts_row].yp_uuid.isNull()) {
	/*
	 * Clear package
	 */
	product->yeasts[product->yeasts_row].yp_uuid = QString();
	product->yeasts[product->yeasts_row].yp_package = QString();
	product->yeasts[product->yeasts_row].yp_cells = product->yeasts[product->yeasts_row].cells;
	product->yeasts[product->yeasts_row].yp_viability = 0.99;
	product->yeasts[product->yeasts_row].yp_max = 100;
	product->yeasts[product->yeasts_row].yp_size = 0.01;
	is_changed();
//	qDebug() << "  cleared old yp_xxx data";
    }
//    qDebug() << "  result not found";
}


void EditProduct::yeast_select_changed(int val)
{
    QSqlQuery query;
    bool instock = yinstockEdit->isChecked();
    QString w;
    QTableWidgetItem *item;
    int oldform = product->yeasts.at(product->yeasts_row).form;

    if (val < 1)
        return;

#ifdef DEBUG_YEAST
    qDebug() << "yeast_select_changed()" << product->yeasts_row << val << instock;
#endif

    /*
     * Search the yeast pointed by the index and instock flag.
     */
    QString sql = "SELECT name,laboratory,product_id,type,form,min_temperature,max_temperature,flocculation,attenuation,"
	          "cells,tolerance,sta1,bacteria,harvest_top,harvest_time,pitch_temperature,pofpos,zymocide,"
		  "gr_hl_lo,sg_lo,gr_hl_hi,sg_hi,cost,inventory FROM inventory_yeasts ";
    if (instock)
        sql.append("WHERE inventory > 0 ");
    sql.append("ORDER BY laboratory,product_id,name");
    query.prepare(sql);
    query.exec();
    query.first();
    for (int i = 0; i < (val - 1); i++) {
        query.next();
    }
#ifdef DEBUG_YEAST
    qDebug() << "found" << query.value("name").toString() << query.value("product_id").toString();
#endif

    /*
     * Replace the yeast record contents
     */
    product->yeasts[product->yeasts_row].name = query.value("name").toString();
    product->yeasts[product->yeasts_row].laboratory = query.value("laboratory").toString();
    product->yeasts[product->yeasts_row].product_id = query.value("product_id").toString();
    product->yeasts[product->yeasts_row].type = query.value("type").toInt();
    product->yeasts[product->yeasts_row].form = query.value("form").toInt();
    product->yeasts[product->yeasts_row].min_temperature = query.value("min_temperature").toDouble();
    product->yeasts[product->yeasts_row].max_temperature = query.value("max_temperature").toDouble();
    product->yeasts[product->yeasts_row].flocculation = query.value("flocculation").toInt();
    product->yeasts[product->yeasts_row].attenuation = query.value("attenuation").toDouble();
    product->yeasts[product->yeasts_row].cells = query.value("cells").toDouble();
    product->yeasts[product->yeasts_row].tolerance = query.value("tolerance").toDouble();
    product->yeasts[product->yeasts_row].sta1 = query.value("sta1").toInt() ? true:false;
    product->yeasts[product->yeasts_row].bacteria = query.value("bacteria").toInt() ? true:false;
    product->yeasts[product->yeasts_row].harvest_top = query.value("harvest_top").toInt() ? true:false;
    product->yeasts[product->yeasts_row].harvest_time = query.value("harvest_time").toInt();
    product->yeasts[product->yeasts_row].pitch_temperature = query.value("pitch_temperature").toDouble();
    product->yeasts[product->yeasts_row].pofpos = query.value("pofpos").toInt() ? true:false;
    product->yeasts[product->yeasts_row].zymocide = query.value("zymocide").toInt();
    product->yeasts[product->yeasts_row].gr_hl_lo = query.value("gr_hl_lo").toInt();
    product->yeasts[product->yeasts_row].sg_lo = query.value("sg_lo").toDouble();
    product->yeasts[product->yeasts_row].gr_hl_hi = query.value("gr_hl_hi").toInt();
    product->yeasts[product->yeasts_row].sg_hi = query.value("sg_hi").toDouble();
    product->yeasts[product->yeasts_row].cost = query.value("cost").toDouble();
    product->yeasts[product->yeasts_row].inventory = query.value("inventory").toDouble();
    product->yeasts[product->yeasts_row].yp_uuid = QString();

    /*
     * Update the visible fields
     */
    const QSignalBlocker blocker1(yamountEdit);
    ynameEdit->setText(product->yeasts.at(product->yeasts_row).name);
    ylaboratoryEdit->setText(product->yeasts.at(product->yeasts_row).laboratory);
    yproduct_idEdit->setText(product->yeasts.at(product->yeasts_row).product_id);
    if (product->yeasts.at(product->yeasts_row).form == YEAST_FORMS_LIQUID) {
	if (oldform != YEAST_FORMS_LIQUID)
	    product->yeasts[product->yeasts_row].amount = 1;
	yamountEdit->setValue(product->yeasts[product->yeasts_row].amount);
	yamountEdit->setDecimals(0);
	yamountEdit->setSingleStep(1.0);
	yamountLabel->setText(tr("Total packs:"));
	product->yeast_pitchrate = 0;
    } else if ((product->yeasts.at(product->yeasts_row).form == YEAST_FORMS_DRY) || (product->yeasts.at(product->yeasts_row).form == YEAST_FORMS_DRIED)) {
	if (oldform == YEAST_FORMS_LIQUID)
	    product->yeasts[product->yeasts_row].amount = 0.01;
	yamountEdit->setValue(product->yeasts[product->yeasts_row].amount * 1000.0);
	yamountEdit->setDecimals(1);
    	yamountEdit->setSingleStep(0.5);
	yamountLabel->setText(tr("Amount in gr:"));
    } else {
	if (oldform == YEAST_FORMS_LIQUID)
	    product->yeasts[product->yeasts_row].amount = 0.01;
	yamountEdit->setValue(product->yeasts[product->yeasts_row].amount * 1000.0);
	yamountEdit->setDecimals(1);
	yamountEdit->setSingleStep(0.5);
	yamountLabel->setText(tr("Amount in ml:"));
	product->yeast_pitchrate = 0;
    }

    /*
     * Clear package and rebuild package select table.
     */
    yeast_load_packages(query.value("laboratory").toString(), query.value("form").toInt());

    ui->yeastsTable->setItem(product->yeasts_row, 0, new QTableWidgetItem(product->yeasts.at(product->yeasts_row).name));
    ui->yeastsTable->setItem(product->yeasts_row, 1, new QTableWidgetItem(product->yeasts.at(product->yeasts_row).laboratory));
    ui->yeastsTable->setItem(product->yeasts_row, 2, new QTableWidgetItem(product->yeasts.at(product->yeasts_row).product_id));

    item = new QTableWidgetItem(QCoreApplication::translate("YeastForm", g_yeast_forms[product->yeasts.at(product->yeasts_row).form]));
    item->setTextAlignment(Qt::AlignCenter|Qt::AlignVCenter);
    ui->yeastsTable->setItem(product->yeasts_row, 3, item);

    item = new QTableWidgetItem(QString("%1").arg(product->yeasts.at(product->yeasts_row).min_temperature, 2, 'f', 1, '0'));
    item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
    ui->yeastsTable->setItem(product->yeasts_row, 5, item);

    item = new QTableWidgetItem(QString("%1").arg(product->yeasts.at(product->yeasts_row).max_temperature, 2, 'f', 1, '0'));
    item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
    ui->yeastsTable->setItem(product->yeasts_row, 6, item);

    item = new QTableWidgetItem(QString("%1").arg(product->yeasts.at(product->yeasts_row).tolerance, 2, 'f', 1, '0'));
    item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
    ui->yeastsTable->setItem(product->yeasts_row, 7, item);

    item = new QTableWidgetItem(QString("%1").arg(product->yeasts.at(product->yeasts_row).attenuation, 2, 'f', 1, '0'));
    item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
    ui->yeastsTable->setItem(product->yeasts_row, 8, item);

    if (product->yeasts.at(product->yeasts_row).use != YEAST_USE_BOTTLE && product->yeasts.at(product->yeasts_row).sta1) {
	QWidget *pWidget = new QWidget();
	QLabel *label = new QLabel;
	label->setPixmap(QPixmap(":icons/silk/tick.png"));
	QHBoxLayout *pLayout = new QHBoxLayout(pWidget);
	pLayout->addWidget(label);
	pLayout->setAlignment(Qt::AlignCenter);
	pLayout->setContentsMargins(0, 0, 0, 0);
	pWidget->setLayout(pLayout);
	ui->yeastsTable->setCellWidget(product->yeasts_row, 9, pWidget);
    } else {
	ui->yeastsTable->removeCellWidget(product->yeasts_row, 9);
    }

    if (product->yeasts.at(product->yeasts_row).form == YEAST_FORMS_LIQUID)
	item = new QTableWidgetItem(QString("%1 pack").arg(product->yeasts.at(product->yeasts_row).amount, 1, 'f', 0, '0'));
    else if (product->yeasts.at(product->yeasts_row).form == YEAST_FORMS_DRY || product->yeasts.at(product->yeasts_row).form == YEAST_FORMS_DRIED)
	item = new QTableWidgetItem(QString("%1 gr").arg(product->yeasts.at(product->yeasts_row).amount * 1000.0, 3, 'f', 2, '0'));
    else
	item = new QTableWidgetItem(QString("%1 ml").arg(product->yeasts.at(product->yeasts_row).amount * 1000.0, 3, 'f', 2, '0'));
    item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter);
    ui->yeastsTable->setItem(product->yeasts_row, 10, item);

    /*
     * If there is no need for a starter, wipe it.
     */
    bool maybe_starter = false;
    for (int i = 0; i < product->yeasts.size(); i++) {
        if (product->yeasts.at(i).use == YEAST_USE_PRIMARY &&
	    ! ((product->yeasts.at(i).form == YEAST_FORMS_DRY) || (product->yeasts.at(i).form == YEAST_FORMS_DRIED)))
            maybe_starter = true;
    }
    if (! maybe_starter) {
#ifdef DEBUG_YEAST
        qDebug() << "  Clear starter (if any)";
#endif
        for (int i = 0; i < 4; i++)
            product->prop_volume[i] = 0;
    }

    is_changed();
}


void EditProduct::yeast_instock_changed(bool val)
{
    QSqlQuery query;

#ifdef DEBUG_YEAST
    qDebug() << "yeast_instock_changed()" << product->yeasts_row << val;
#endif

    this->yselectEdit->setCurrentIndex(-1);
    this->yselectEdit->clear();
    QString sql = "SELECT name,laboratory,product_id,inventory FROM inventory_yeasts ";
    if (val)
        sql.append("WHERE inventory > 0 ");
    sql.append("ORDER BY laboratory,product_id,name");
    query.prepare(sql);
    query.exec();
    query.first();
    this->yselectEdit->addItem("");      // Start with empty value
    for (int i = 0; i < query.size(); i++) {
        this->yselectEdit->addItem(query.value(1).toString()+" - "+query.value(2).toString()+" "+query.value(0).toString() + 
                        QString(" (%1 gr)").arg(query.value(3).toDouble() * 1000.0, 2, 'f', 1, '0'));
        query.next();
    }
}


void EditProduct::yeast_useat_changed(int val)
{
#ifdef DEBUG_YEAST
    qDebug() << "yeast_useat_changed()" << product->yeasts_row << val;
#endif

    product->yeasts[product->yeasts_row].use = val;
    QTableWidgetItem *item = new QTableWidgetItem(QCoreApplication::translate("YeastUse", g_yeast_use[val]));
    item->setTextAlignment(Qt::AlignCenter|Qt::AlignVCenter);
    ui->yeastsTable->setItem(product->yeasts_row, 4, item);
    is_changed();
}


void EditProduct::editYeastRow_clicked()
{
    QSqlQuery query;

    if (product->locked)
	return;

    QPushButton *pb = qobject_cast<QPushButton *>(QObject::sender());
    product->yeasts_row = pb->objectName().toInt();
#ifdef DEBUG_YEAST
    qDebug() << "Edit yeast row" << product->yeasts_row;
#endif
    Yeasts backup = product->yeasts.at(product->yeasts_row);

    QDialog* dialog = new QDialog(this);
    dialog->resize(738, 290);
    QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog);
    buttonBox->setObjectName(QString::fromUtf8("buttonBox"));
    buttonBox->setGeometry(QRect(30, 240, 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("Yeast name:"));
    nameLabel->setGeometry(QRect(10, 10, 141, 20));
    nameLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
    QLabel *laboratoryLabel = new QLabel(dialog);
    laboratoryLabel->setObjectName(QString::fromUtf8("laboratoryLabel"));
    laboratoryLabel->setText(tr("Laboratory:"));
    laboratoryLabel->setGeometry(QRect(10, 40, 141, 20));
    laboratoryLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
    QLabel *product_idLabel = new QLabel(dialog);
    product_idLabel->setObjectName(QString::fromUtf8("product_idLabel"));
    product_idLabel->setText(tr("Laboratory:"));
    product_idLabel->setGeometry(QRect(10, 70, 141, 20));
    product_idLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
    QLabel *selectLabel = new QLabel(dialog);
    selectLabel->setObjectName(QString::fromUtf8("selectLabel"));
    selectLabel->setText(tr("Select yeast:"));
    selectLabel->setGeometry(QRect(10,100, 141, 20));
    selectLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
    QLabel *instockLabel = new QLabel(dialog);
    instockLabel->setObjectName(QString::fromUtf8("instockLabel"));
    instockLabel->setText(tr("In stock:"));
    instockLabel->setGeometry(QRect(525,100, 121, 20));
    instockLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
    QLabel *packageLabel = new QLabel(dialog);
    packageLabel->setObjectName(QString::fromUtf8("packageLabel"));
    packageLabel->setText(tr("Select package:"));
    packageLabel->setGeometry(QRect(10,130, 141, 20));
    packageLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
    yamountLabel = new QLabel(dialog);
    yamountLabel->setObjectName(QString::fromUtf8("amountLabel"));
    if (product->yeasts.at(product->yeasts_row).form == YEAST_FORMS_LIQUID)
	yamountLabel->setText(tr("Total packs:"));
    else if ((product->yeasts.at(product->yeasts_row).form == YEAST_FORMS_DRY) || (product->yeasts.at(product->yeasts_row).form == YEAST_FORMS_DRIED))
	yamountLabel->setText(tr("Amount in gr:"));
    else
	yamountLabel->setText(tr("Amount in ml:"));
    yamountLabel->setGeometry(QRect(10, 160, 141, 20));
    yamountLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
    QLabel *useatLabel = new QLabel(dialog);
    useatLabel->setObjectName(QString::fromUtf8("useatLabel"));
    useatLabel->setText(tr("Use at:"));
    useatLabel->setGeometry(QRect(10, 190, 141, 20));
    useatLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);

    ynameEdit = new QLineEdit(dialog);
    ynameEdit->setObjectName(QString::fromUtf8("ynameEdit"));
    ynameEdit->setText(product->yeasts.at(product->yeasts_row).name);
    ynameEdit->setGeometry(QRect(160, 10, 511, 23));
    ynameEdit->setReadOnly(true);
    ylaboratoryEdit = new QLineEdit(dialog);
    ylaboratoryEdit->setObjectName(QString::fromUtf8("ylaboratoryEdit"));
    ylaboratoryEdit->setText(product->yeasts.at(product->yeasts_row).laboratory);
    ylaboratoryEdit->setGeometry(QRect(160, 40, 511, 23));
    ylaboratoryEdit->setReadOnly(true);
    yproduct_idEdit = new QLineEdit(dialog);
    yproduct_idEdit->setObjectName(QString::fromUtf8("yproduct_idEdit"));
    yproduct_idEdit->setText(product->yeasts.at(product->yeasts_row).product_id);
    yproduct_idEdit->setGeometry(QRect(160, 70, 511, 23));
    yproduct_idEdit->setReadOnly(true);
    yselectEdit = new QComboBox(dialog);
    yselectEdit->setObjectName(QString::fromUtf8("selectEdit"));
    yselectEdit->setGeometry(QRect(160,100, 371, 23));
    yinstockEdit = new QCheckBox(dialog);
    yinstockEdit->setObjectName(QString::fromUtf8("yinstockEdit"));
    yinstockEdit->setGeometry(QRect(655,100, 85, 21));
    yinstockEdit->setChecked(true);
    ypackageEdit = new QComboBox(dialog);
    ypackageEdit->setObjectName(QString::fromUtf8("packageEdit"));
    ypackageEdit->setGeometry(QRect(160,130, 371, 23));
    yamountEdit = new QDoubleSpinBox(dialog);
    yamountEdit->setObjectName(QString::fromUtf8("yamountEdit"));
    yamountEdit->setGeometry(QRect(160, 160, 121, 24));
    yamountEdit->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter);
    yamountEdit->setAccelerated(true);
    yamountEdit->setMaximum(10000.0);
    if (product->yeasts.at(product->yeasts_row).form == YEAST_FORMS_LIQUID) {
	yamountEdit->setDecimals(0);
        yamountEdit->setSingleStep(1.0);
	yamountEdit->setValue(product->yeasts.at(product->yeasts_row).amount);
    } else if ((product->yeasts.at(product->yeasts_row).form == YEAST_FORMS_DRY) || (product->yeasts.at(product->yeasts_row).form == YEAST_FORMS_DRIED)) {
    	yamountEdit->setDecimals(1);
    	yamountEdit->setSingleStep(0.5);
	yamountEdit->setValue(product->yeasts.at(product->yeasts_row).amount * 1000.0);
    } else {
	yamountEdit->setDecimals(1);
        yamountEdit->setSingleStep(0.5);
	yamountEdit->setValue(product->yeasts.at(product->yeasts_row).amount * 1000.0);
    }
    yamountEdit->setMaximum(1000000000.0);
    useatEdit = new QComboBox(dialog);
    useatEdit->setObjectName(QString::fromUtf8("useatEdit"));
    useatEdit->setGeometry(QRect(160, 190, 161, 23));
    useatEdit->addItem(tr("Primary"));
    useatEdit->addItem(tr("Secondary"));
    useatEdit->addItem(tr("Tertiary"));
    useatEdit->addItem(tr("Bottle"));
    useatEdit->setCurrentIndex(product->yeasts.at(product->yeasts_row).use);

    yeast_instock_changed(true);
    yeast_load_packages(product->yeasts.at(product->yeasts_row).laboratory, product->yeasts.at(product->yeasts_row).form);

    /*
     * Try to set the yeastpack dropdown to the selected entry if set.
     */
    int index = 0;
    if (! product->yeasts.at(product->yeasts_row).yp_uuid.isNull() && (product->yeasts.at(product->yeasts_row).yp_uuid.length() == 36)) {
	query.prepare("SELECT * FROM inventory_yeastpack WHERE laboratory=:laboratory AND form=:form AND valid='1' ORDER BY laboratory,package");
	query.bindValue(":laboratory", product->yeasts.at(product->yeasts_row).laboratory);
	query.bindValue(":form", product->yeasts.at(product->yeasts_row).form);
	query.exec();
	while (query.next()) {
	    index++;
	    if (query.value("uuid").toString() == product->yeasts.at(product->yeasts_row).yp_uuid) {
		this->ypackageEdit->setCurrentIndex(index);
		break;
	    }
	}
    }

    connect(yselectEdit, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &EditProduct::yeast_select_changed);
    connect(ypackageEdit, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &EditProduct::yeast_package_changed);
    connect(yamountEdit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &EditProduct::yeast_amount_changed);
    connect(useatEdit, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &EditProduct::yeast_useat_changed);
    connect(yinstockEdit, &QCheckBox::stateChanged, this, &EditProduct::yeast_instock_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) {
#ifdef DEBUG_YEAST
        qDebug() << "reject and rollback";
#endif
        product->yeasts[product->yeasts_row] = backup;
    } else {

    }

    disconnect(yselectEdit, nullptr, nullptr, nullptr);
    disconnect(ypackageEdit, nullptr, nullptr, nullptr);
    disconnect(yamountEdit, nullptr, nullptr, nullptr);
    disconnect(useatEdit, nullptr, nullptr, nullptr);
    disconnect(yinstockEdit, nullptr, nullptr, nullptr);
    disconnect(buttonBox, nullptr, nullptr, nullptr);

    emit refreshAll();
}


void EditProduct::adjustYeasts(double factor)
{
    double amount;

    if (product->yeasts.size() == 0)
	return;

    for (int i = 0; i < product->yeasts.size(); i++) {
	if (product->yeasts.at(i).form == YEAST_FORMS_DRY) { // Only adjust dry yeast
	    amount = product->yeasts.at(i).amount * factor;
	    product->yeasts[i].amount = amount;
	}
    }
}

mercurial