Version 0.2.0 merged again. stable

Sun, 12 May 2019 19:41:18 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Sun, 12 May 2019 19:41:18 +0200
branch
stable
changeset 366
a14a31bfc73b
parent 365
9f27c822b14d (current diff)
parent 364
487274c2e9dc (diff)
child 377
6e80a5515dd3

Version 0.2.0 merged again.

config.status file | annotate | diff | comparison | revisions
configure file | annotate | diff | comparison | revisions
configure.ac file | annotate | diff | comparison | revisions
doc/bms-ch3.sgml file | annotate | diff | comparison | revisions
www/prod_reduce.php file | annotate | diff | comparison | revisions
--- a/README.design	Sun May 12 19:24:34 2019 +0200
+++ b/README.design	Sun May 12 19:41:18 2019 +0200
@@ -1,118 +1,7 @@
-Centrale daemon 'bmsd' regelt de dagelijkse berichten tussen MQTT en de
-database. Om deze berichten betrouwbaar af te handelen is MQTT er tussen
-gezet.
-Alle sensoren en controllers communiceren uitsluitend via MQTT.
-
-Sensoren en controllers:
-
-  1.  Vergisting controllers.
-  2.  Temperatuur loggen.
-  3.  Hergisting drukmeters. (druk + temp).
-  4.  Brouw controllers.
-  5.  Ispindel.
-
-
-Stappen:
-
-  1.  Importeren oude vergisting en brouw logs.  DONE.
-  2.  Thermferm moet DLOG berichten gaan sturen, per wijziging en per 5 minuten.  DONE.
-  3.  bmsd moet deze DLOG berichten verwerken. Versie 0.0.2. Bier producten in database. DONE.
-  4.  Versie 0.0.2 handmatig installeren op productie. DONE.
-  5.  Thermferm uitbreiden met vergisting stage. DONE.
-  6.  Brouw controller uitbreiden met MQTT. Niet, ESP32 wordt onstabiel.
-  7.  bmsd uitbreiden met brouw controller berichten. Niet, zie hierboven.
-  8.  bmsd productie platform upgraden, versie 0.0.3 DONE.
-  9.  bmsd recepten editor implementeren. DONE.
- 10.  bmsd uitbreiden met productie (brews) stappen, met hierin recepten. DONE.
- 11.  bmsd koppelen logs aan productie. DONE.
-
-
-
-
-MQTT structuur volgens Sparkplug model.
-
-mbv1.0/fermenters/[NBIRTH,DBIRTH]/<hostname>/
-mbv1.0/fermenters/DDATA/<hostname>/<unitalias>/
-
----------------------------------------------------------------------------
-
-namespace/group_id/message_type/edge_node_id/{device_id}
-
-namespace is de root, "mbv1.0"
-
-group_id oa: fermenters, brewcontrol, env_sensor
-
-message_type:	NBIRTH	- Birth certificate for MQTT EoN nodes.
-		NDEATH	- Death certificate for MQTT EoN nodes.
-		NCMD	- Node command message.
-		DBIRTH	- Birth certificate for devices.
-		DDEATH	- Death certificate for devices.
-		NDATA	- Node data message.
-		DDATA	- Device data message.
-		DLOG	- Device data logging.
-		DCMD	- Device command message.
-		STATE	- Critical application state message.
-
-NCMD:	reboot (application restart)
-	rebirth
-
-DCMD:	fermenter state change + temperature settings.
-	fermenter profile install.
-	fermenter load product.
-	fermenter set stage
-
-
-Product: code, uuid en naam.
-	Stage:  Plan Wait Brew Primary Secondary Tertiary Package Carbonation Mature Taste Ready Closed
-	                    |     |       |         |        |          |
-	                    |     |       |         |        |          +-------------	Log/rapport
-	                    |     |       |         |        +------------------------	rapport/etiketten
-	                    |     +-------+---------+---------------------------------	Log/rapport
-	                    +---------------------------------------------------------	Log/rapport
 
 	Main table: products.
-	  In progress: overview.
-	  In Progress: view charts. DONE fermenters, todo brewlogs.
 	  In Progress: view logs. 
-	  In Progress: update state.
-	  In Progress: Tabbed screens. DONE.
-          Start new: some sort of wizzard like a new recipe. DONE.
-	  Archive: select via name/code/date.
 	  Calendar: shows upcoming events.
-	  Recipes can be copied to 'recipes' or 'brews', imported from 'recipes'/'products' or created manual.
-          Products recipes Beerxml import.
-	  Recipes Beerxml import.
-
-
------------------------------------------------------------------------------
-
-	Formaat csv vergisting logs.
-
-Directory:	www/logs/fermentation
-Filenaam:	product_code\ product_name.log
-
-   2014-11-15 18:39,BEER,PRIMARY,20.312,19.750,-1.500,20.5,18.6,18.8,35,12345,0,67890,Whatsup,Fermenter
-            |         |     |      |      |      |      |    |    |   |   |   |   |      |        |
- 0 datetime +         |     |      |      |      |      |    |    |   |   |   |   |      |        |
- 1 mode --------------+     |      |      |      |      |    |    |   |   |   |   |      |        |
- 2 stage -------------------+      |      |      |      |    |    |   |   |   |   |      |        |
- 3 temp air -----------------------+      |      |      |    |    |   |   |   |   |      |        |
- 4 temp beer -----------------------------+      |      |    |    |   |   |   |   |      |        |
- 5 temp chiller ---------------------------------+      |    |    |   |   |   |   |      |        |
- 6 temp room -------------------------------------------+    |    |   |   |   |   |      |        |
- 7 setpoint low ---------------------------------------------+    |   |   |   |   |      |        |
- 8 setpoint high -------------------------------------------------+   |   |   |   |      |        |
- 9 heater power ------------------------------------------------------+   |   |   |      |        |
-10 heater usage ----------------------------------------------------------+   |   |      |        |
-11 cooler power --------------------------------------------------------------+   |      |        |
-12 cooler usage ------------------------------------------------------------------+      |        |
-13 event --------------------------------------------------------------------------------+        |
-14 fermenter uuid --------------------------------------------------------------------------------+
-
-De oude logs zijn geimporteerd en geconverteerd. Nieuwe worden geschreven
-door bmsd welke de log gegevens ontvangt via MQTT DLOG berichten.
-Vanwege de snelheid van verwerken staan de logs niet in de database.
-Kunnen we de loggegevens versturen in gzip formaat om bandbreedte te sparen?
 
 -----------------------------------------------------------------------------
 
@@ -122,10 +11,6 @@
 
 Extra:
 
-Gisten alcohol_tolerance veld toevoegen.
 Gisten diastaticus bit toevoegen.
-Waarschuwing voor te zwaar voor de gist.
-Waarschuwing voor overschrijden moutstort.
 Gist typen: kveik en brett? Apart of niet.
 
-
--- a/doc/Makefile	Sun May 12 19:24:34 2019 +0200
+++ b/doc/Makefile	Sun May 12 19:41:18 2019 +0200
@@ -3,7 +3,8 @@
 
 include ../Makefile.global
 
-SRC		= bms.sgml bms-ch1.sgml bms-ch2.sgml bms-ch3.sgml
+SRC		= bms.sgml bms-ch1.sgml bms-ch2.sgml bms-ch3.sgml bms-ch4.sgml \
+		  bms-ch5.sgml bms-ch6.sgml bms-ch7.sgml bms-ch8.sgml
 HTML		= bms.html
 PDF		= bms.pdf
 OTHER		= Makefile docbook-utils.dsl 
--- a/doc/bms-ch3.sgml	Sun May 12 19:24:34 2019 +0200
+++ b/doc/bms-ch3.sgml	Sun May 12 19:41:18 2019 +0200
@@ -2,315 +2,10 @@
   vim:syntax=docbksgml
 -->
 
-<chapter id="protocols">
-<title>Protocollen.</title>
+<chapter id="instellingen">
+<title>Instellingen.</title>
 <para>
-De netwerk protocollen.
+Instellingen tekst.
 </para>
 
-<sect1 id="prototopic">
-<title>MQTT topic formaat.</title>
-<para>De topics zijn als volgt gedefinieerd:</para>
-<programlisting>
-mbv1.0/<code>group_id</code>/<code>message_type</code>/<code>edge_node</code>/<code>device_id</code>
-</programlisting>
-
-<itemizedlist>
-<listitem><para><code>group_id</code> geeft het type apparaat aan zoals <code>fermenters</code>
-en <code>brewcontrol</code>.</para></listitem>
-<listitem><para><code>message_type</code> geeft het bericht type aan zoals
-<code>NBIRTH</code>, <code>DDATA</code>.</para></listitem>
-<listitem><para><code>edge_node</code> is de hostnaam van de node die het bericht stuurt. Dit is
-de naam zonder domain toevoeging.</para></listitem>
-<listitem><para><code>device_id</code> is de verkorte naam van het apparaat module waarvan dit
-bericht komt zoals de naam van een vergisting controller. Dit is niet aanwezig met NODE berichten.</para></listitem>
-</itemizedlist>
-
-<para>
-De volgende <code>group_id</code> namen zijn gedefinieerd:
-</para>
-<orderedlist>
-<listitem><para>brewery is voor de bms applicatie zelf. Nog uitwerken.</para></listitem>
-<listitem><para>fermenters is voor vergisting controllers.</para></listitem>
-<listitem><para>brewcontrol is een brouw controller. Deze controller kan een deel
-of geheel brouwproces uitvoeren.</para></listitem>
-<listitem><para>pressure is een drukmeter om bijvoorbeeld hergisting op de fles
-te monitoren.</para></listitem>
-</orderedlist>
-
-<para>De volgende <code>message_type</code> namen zijn gedefinieerd:</para>
-<orderedlist>
-<listitem><para>NBIRTH geeft aan wanneer een node opstart en met het netwerk
-verbonden is. Dit is een zogenaamd persistent bericht, het blijft voor nieuwe
-MQTT clients altijd zichtbaar. Bij het starten van een node wordt er een payload
-verzonden, zie het payload formaat voor een node. Als een node afsluit wordt juist geen
-payload verzonden zodat het bericht verdwijnt.</para></listitem>
-<listitem><para>NDATA wordt verstuurd als er veranderingen zijn voor de node,
-maar ook iedere vijf minuten om aan te geven dat de node nog "levend" en aanwezig
-is.</para></listitem>
-<listitem><para>NDEATH wordt verstuurd als een node offline gaat. Maar het kan ook
-ontvangen worden als de MQTT verbinding verbroken wordt met een node, het NDEATH
-bericht is ook het `last will' bericht van een node. Maar als het echt fout gaat
-dan zal er mogenlijk nooit een NDEATH bericht gezien worden.</para></listitem>
-<listitem><para>NCMD is een commando bestemd voor een node.
-Dit kan bijvoorbeeld een reboot commando zijn.</para></listitem>
-<listitem><para>DBIRTH is een of meer berichten van een apparaat wat online komt
-en ingeschakeld is. Een apparaat is een deel van een node.
-Bij het opstarten van de node is er geen <code>device_id</code>
-omdat alle apparaten is een keer verstuurd worden. Indien er later een enkel apparaat
-ingeschakeld wordt dan is er wel een geldige <code>device_id</code> aanwezig.
-Hier ook weer, er is een payload bij opstarten en geen payload bij afsluiten om het
-persistente bericht goed te houden.</para></listitem>
-<listitem><para>DDATA heeft altijd een payload, maar deze hoeft niet volledig te zijn,
-enkel de gewijzigde data moet in het bericht zitten.</para></listitem>
-<listitem><para>DDEATH wordt verstuurd als een node offline gaat, of als het apparaat
-uitgeschakelt wordt.</para></listitem>
-<listitem><para>DLOG is een data log. Hier bestaat de payload uit gegevens die de
-bms applicatie in de database zet.</para></listitem>
-<listitem><para>DCMD is een commando voor een apparaat wat op een node geinstalleerd is.
-Dit zullen voornamelijk instellingen voor dat enkele apparaat zijn.</para></listitem>
-</orderedlist>
-
-</sect1>
-
-
-<sect1 id="payloadnode">
-<title>Netwerk payload formaat voor een node</title>
-<para>
-De payload zoals die door een node verstuurd wordt. Het wordt in json formaat
-verzonder zonder extra spaties en opmaak zoals hieronder is te zien. Het timestamp
-is de unix tijd sinds 1 januari 1970. Het `seq' nummer wordt met ieder bericht met 1
-verhoogd.</para>
-
-<programlisting>
-{
-  "timestamp": 1532201089,
-  "seq": 0,
-  "metric": {
-    "uuid": "b508f01c-1f82-4e8b-b0d2-d88ecfb53031",
-    "properties": {
-      "hardwaremake": "Raspberry",
-      "hardwaremodel": "Unknown",
-      "os": "Linux",
-      "os_version": "4.1.19+",
-      "FW": "0.8.2"
-    },
-    "THB": {
-      "temperature": 20.0,
-      "humidity": 50.0,
-      "barometer": 1002
-    },
-    "GPS": {
-      "latitude": 1.2345,
-      "longitude": 2.3456,
-      "altitude": 20
-    },
-    "net": {
-      "address": "10.126.151.11",
-      "ifname": "eth0",
-      "rssi": 0
-    }
-  }
-}
-</programlisting>
-</sect1>
-
-<sect1 id="cmdnode">
-<title>Netwerk kommando payload formaat voor nodes.</title>
-<para>De volgende kommando's kunnen gestuurd worden naar nodes:</para>
-<programlisting>
-{
-  "timestamp":1532201089,
-  "metric": {
-    "Node Control/Reboot":true
-  }
-}
-</programlisting>
-<para>Dit commando reboot niet de computer maar de applicatie die op een
-computer zoals een Raspberry Pi geinstalleerd is. Een uitzondering zijn de
-controllers zoals Arduino's en andere eenvoudige systemen.</para>
-<programlisting>
-{
-  "timestamp":1532201089,
-  "metric": {
-    "Node Control/Rebirth":true
-  }
-}
-</programlisting>
-<para>Dit commando zorgt er voor dat alle NBIRTH en DBIRTH berichten opnieuw
-verzonden worden alsof de computer net is opgestart. Dit kan nuttig zijn na
-een herstart van de bms applicatie zelf zodat de juiste nodes informatie weer
-beschikbaar is.</para>
-</sect1>
-
-<sect1 id="payloadfermdata">
-<title>Netwerk payload data formaat voor vergisting controllers</title>
-<para>
-Dit is het meest uitgebreide formaat wat getoond is. Indien er bijvoorbeeld geen
-chiller aanwezig is, dan wordt eem `null' gestuurd in plaats van een json blok
-met gegevens. De werkelijke uitvoering van de hardware en de configuratie daarvan
-bepaald dus het uiteindelijke payload formaat.
-</para>
-<programlisting>
-{
-  "uuid": "48c9ae27-3f58-41c9-ae4b-1d57b249c45a",
-  "alias": "unit1",
-  "product": {
-    "uuid": "1eb0c7bf-bf06-491c-a086-ac5478d521b9",
-    "code": "CB0001",
-    "name": "Hoppy Housebeer"
-  },
-  "air": {
-    "address": "70d60411-3ec8-40ab-998a-81fead83025f",
-    "state": "OK",
-    "temperature": 21.562
-  },
-  "beer": {
-    "address": "8ec36f9d-f382-4e32-a47f-732642e1018d",
-    "state": "OK",
-    "temperature": 22.125
-  },
-  "chiller": {
-    "address": "e81265b8-07f7-4b22-96c1-6f55a4b66a83",
-    "state": "OK",
-    "temperature": 12.437
-  },
-  "heater": {
-    "address": "d2f2d6bc-4d12-4852-9462-95f4c2476034",
-    "state": 0,
-    "usage": 10710793
-  },
-  "cooler": {
-    "address": "a9f30140-812c-4ec1-9e98-3a9d47deff7c",
-    "state": 0,
-    "usage": 920504
-  },
-  "fan": {
-    "address": "ae9f9887-8209-4810-9f58-ddfb34ee142f",
-    "state": 100,
-    "usage": 62889739
-  },
-  "light": {
-    "address": "cc6353cf-9c97-41b9-b6cf-00cea312e478",
-    "state": 0,
-    "usage": 29647290
-  },
-  "door": {
-    "address": "ad8746d1-0549-485a-a215-41e5cdde9e75",
-    "state": 1
-  },
-  "psu": {
-    "address": "e1bb7182-883d-4977-a1c0-76e214072fc5",
-    "state": 1
-  },
-  "stage": "PRIMARY",
-  "mode": "BEER",
-  "setpoint": {
-    "low": 21.0,
-    "high": 21.0
-  },
-  "webcam": {
-    "url":"https://the.webcamserver.com:8090/?action=stream",
-    "light": 1
-  },
-  "alarm": 0,
-  "profile": {
-    "uuid": "c93ad1bb-0446-4788-9c43-83990c5f8b82",
-    "name": "Witbier methode Cellis",
-    "state": "OFF",
-    "percent": 0,
-    "inittemp": {
-      "low": 17.9,
-      "high": 18.1
-    },
-    "fridgemode": 0,
-    "steps": [
-      {
-        "resttime": 2,
-        "steptime": 0,
-        "target": {
-          "low": 18.0,
-          "high": 18.0
-        },
-        "fridgemode": 0
-      },
-      {
-        "resttime": 0,
-        "steptime": 24,
-        "target": {
-          "low": 18.0,
-          "high": 22.0
-        },
-        "fridgemode": 0
-      },
-      {
-        "resttime": 48,
-        "steptime": 96,
-        "target": {
-          "low": 26.0,
-          "high": 26.0
-        },
-        "fridgemode": 0
-      }
-    ]
-  }
-}
-</programlisting>
-<para>Temperature states can be: OK, MISSING or ERROR.<para>
-<para>The general `mode' can be: OFF, NONE, FRIDGE, BEER or PROFILE.</para>
-<para>The profile `state' can be: OFF, PAUSE, RUN, DONE or ABORT.</para>
-</sect1>
-
-
-<sect1 id="payloadfermlog">
-<title>Netwerk payload log formaat voor vergisting controllers</title>
-<para>
-Dit is het meest uitgebreide formaat wat getoond is. Indien er bijvoorbeeld geen
-chiller aanwezig is, dan wordt geen data hiervoor verstuurd.
-De werkelijke uitvoering van de hardware en de configuratie daarvan
-bepaald dus het uiteindelijke payload formaat.
-</para>
-<programlisting>
-{
-  "timestamp": 1532201089,
-  "seq": 0,
-  "metric": {
-    "product": {
-      "uuid": "1eb0c7bf-bf06-491c-a086-ac5478d521b9",
-      "code": "CB0001",
-      "name": "Hoppy Housebeer"     
-    },
-    "stage": "PRIMARY",
-    "mode": "BEER",
-    "temperature": {
-      "air": 20.125,
-      "beer": 20.062,
-      "chiller": -3.000,
-      "room": 20.1
-    },
-    "setpoint": {
-      "low": 19.8,
-      "high": 20.1
-    },
-    "heater": {
-      "power": 100,
-      "usage": 1234
-    },
-    "cooler": {
-      "power": 0,
-      "usage": 27273
-    },
-    "fan": {
-      "power": 100,
-      "usage": 8273772
-    },
-    "sg": 1.023,
-    "event": "Something to mark",
-    "fermenter_uuid": "48c9ae27-3f58-41c9-ae4b-1d57b249c45a"
-  }
-}
-</programlisting>
-</sect1>
-
-
 </chapter>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/bms-ch4.sgml	Sun May 12 19:41:18 2019 +0200
@@ -0,0 +1,11 @@
+<!-- 
+  vim:syntax=docbksgml
+-->
+
+<chapter id="inventaris">
+<title>Inventaris.</title>
+<para>
+Inventaris tekst.
+</para>
+
+</chapter>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/bms-ch5.sgml	Sun May 12 19:41:18 2019 +0200
@@ -0,0 +1,11 @@
+<!-- 
+  vim:syntax=docbksgml
+-->
+
+<chapter id="monitoren">
+<title>Monitoren.</title>
+<para>
+Monitoren tekst.
+</para>
+
+</chapter>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/bms-ch6.sgml	Sun May 12 19:41:18 2019 +0200
@@ -0,0 +1,33 @@
+<!-- 
+  vim:syntax=docbksgml
+-->
+
+<chapter id="productie">
+<title>Productie.</title>
+<para>
+Productie tekst.
+</para>
+
+<sect1 id="prodfases">
+<title>Productie fases.</title>
+<para>Het productie proces is verdeeld in de volgende stappen:</para>
+
+<orderedlist>
+<listitem><para>Planning.</para></listitem>
+<listitem><para>Wachten.</para></listitem>
+<listitem><para>Brouwen.</para></listitem>
+<listitem><para>Hoofdgisting.</para></listitem>
+<listitem><para>Nagisting.</para></listitem>
+<listitem><para>Lageren.</para></listitem>
+<listitem><para>Verpakken.</para></listitem>
+<listitem><para>Hergisten.</para></listitem>
+<listitem><para>Rijpen.</para></listitem>
+<listitem><para>Proeven.</para></listitem>
+<listitem><para>Gereed.</para></listitem>
+<listitem><para>Afgesloten.</para></listitem>
+</orderedlist>
+
+
+</sect1>
+
+</chapter>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/bms-ch7.sgml	Sun May 12 19:41:18 2019 +0200
@@ -0,0 +1,11 @@
+<!-- 
+  vim:syntax=docbksgml
+-->
+
+<chapter id="recepten">
+<title>Recepten.</title>
+<para>
+Recepten tekst.
+</para>
+
+</chapter>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/bms-ch8.sgml	Sun May 12 19:41:18 2019 +0200
@@ -0,0 +1,343 @@
+<!-- 
+  vim:syntax=docbksgml
+-->
+
+<chapter id="protocols">
+<title>Protocollen.</title>
+<para>
+De netwerk protocollen.
+</para>
+
+<sect1 id="prototopic">
+<title>MQTT topic formaat.</title>
+<para>De topics zijn als volgt gedefinieerd:</para>
+<programlisting>
+mbv1.0/<code>group_id</code>/<code>message_type</code>/<code>edge_node</code>/<code>device_id</code>
+</programlisting>
+
+<itemizedlist>
+<listitem><para><code>group_id</code> geeft het type apparaat aan zoals <code>fermenters</code>
+en <code>brewcontrol</code>.</para></listitem>
+<listitem><para><code>message_type</code> geeft het bericht type aan zoals
+<code>NBIRTH</code>, <code>DDATA</code>.</para></listitem>
+<listitem><para><code>edge_node</code> is de hostnaam van de node die het bericht stuurt. Dit is
+de naam zonder domain toevoeging.</para></listitem>
+<listitem><para><code>device_id</code> is de verkorte naam van het apparaat module waarvan dit
+bericht komt zoals de naam van een vergisting controller. Dit is niet aanwezig met NODE berichten.</para></listitem>
+</itemizedlist>
+
+<para>
+De volgende <code>group_id</code> namen zijn gedefinieerd:
+</para>
+<orderedlist>
+<listitem><para>brewery is voor de bms applicatie zelf. Nog uitwerken.</para></listitem>
+<listitem><para>fermenters is voor vergisting controllers.</para></listitem>
+<listitem><para>brewcontrol is een brouw controller. Deze controller kan een deel
+of geheel brouwproces uitvoeren.</para></listitem>
+<listitem><para>pressure is een drukmeter om bijvoorbeeld hergisting op de fles
+te monitoren.</para></listitem>
+</orderedlist>
+
+<para>De volgende <code>message_type</code> namen zijn gedefinieerd:</para>
+<orderedlist>
+<listitem><para>NBIRTH geeft aan wanneer een node opstart en met het netwerk
+verbonden is. Dit is een zogenaamd persistent bericht, het blijft voor nieuwe
+MQTT clients altijd zichtbaar. Bij het starten van een node wordt er een payload
+verzonden, zie het payload formaat voor een node. Als een node afsluit wordt juist geen
+payload verzonden zodat het bericht verdwijnt.</para></listitem>
+<listitem><para>NDEATH wordt verstuurd als een node offline gaat. Maar het kan ook
+ontvangen worden als de MQTT verbinding verbroken wordt met een node, het NDEATH
+bericht is ook het `last will' bericht van een node. Maar als het echt fout gaat
+dan zal er mogenlijk nooit een NDEATH bericht gezien worden.</para></listitem>
+<listitem><para>NCMD is een commando bestemd voor een node.
+Dit kan bijvoorbeeld een reboot commando zijn.</para></listitem>
+<listitem><para>NDATA wordt verstuurd als er veranderingen zijn voor de node,
+maar ook iedere vijf minuten om aan te geven dat de node nog "levend" en aanwezig
+is.</para></listitem>
+
+<listitem><para>DBIRTH is een of meer berichten van een apparaat wat online komt
+en ingeschakeld is. Een apparaat is een deel van een node.
+Bij het opstarten van de node is er geen <code>device_id</code>
+omdat alle apparaten is een keer verstuurd worden. Indien er later een enkel apparaat
+ingeschakeld wordt dan is er wel een geldige <code>device_id</code> aanwezig.
+Hier ook weer, er is een payload bij opstarten en geen payload bij afsluiten om het
+persistente bericht goed te houden.</para></listitem>
+<listitem><para>DDEATH wordt verstuurd als een node offline gaat, of als het apparaat
+uitgeschakelt wordt.</para></listitem>
+<listitem><para>DDATA heeft altijd een payload, maar deze hoeft niet volledig te zijn,
+enkel de gewijzigde data moet in het bericht zitten.</para></listitem>
+<listitem><para>DLOG is een data log. Hier bestaat de payload uit gegevens die de
+bms applicatie in de database zet.</para></listitem>
+<listitem><para>DCMD is een commando voor een apparaat wat op een node geinstalleerd is.
+Dit zullen voornamelijk instellingen voor dat enkele apparaat zijn.</para></listitem>
+</orderedlist>
+
+</sect1>
+
+
+<sect1 id="payloadnode">
+<title>Netwerk payload formaat voor een node</title>
+<para>
+De payload zoals die door een node verstuurd wordt. Het wordt in json formaat
+verzonder zonder extra spaties en opmaak zoals hieronder is te zien. Het timestamp
+is de unix tijd sinds 1 januari 1970. Het `seq' nummer wordt met ieder bericht met 1
+verhoogd.</para>
+
+<programlisting>
+{
+  "timestamp": 1532201089,
+  "seq": 0,
+  "metric": {
+    "uuid": "b508f01c-1f82-4e8b-b0d2-d88ecfb53031",
+    "properties": {
+      "hardwaremake": "Raspberry",
+      "hardwaremodel": "Unknown",
+      "os": "Linux",
+      "os_version": "4.1.19+",
+      "FW": "0.8.2"
+    },
+    "THB": {
+      "temperature": 20.0,
+      "humidity": 50.0,
+      "barometer": 1002
+    },
+    "GPS": {
+      "latitude": 1.2345,
+      "longitude": 2.3456,
+      "altitude": 20
+    },
+    "net": {
+      "address": "10.126.151.11",
+      "ifname": "eth0",
+      "rssi": 0
+    }
+  }
+}
+</programlisting>
+</sect1>
+
+<sect1 id="cmdnode">
+<title>Netwerk kommando payload formaat voor nodes.</title>
+<para>De volgende kommando's kunnen gestuurd worden naar nodes:</para>
+<programlisting>
+{
+  "timestamp":1532201089,
+  "metric": {
+    "Node Control/Reboot":true
+  }
+}
+</programlisting>
+<para>Dit commando reboot niet de computer maar de applicatie die op een
+computer zoals een Raspberry Pi geinstalleerd is. Een uitzondering zijn de
+controllers zoals Arduino's en andere eenvoudige systemen.</para>
+<programlisting>
+{
+  "timestamp":1532201089,
+  "metric": {
+    "Node Control/Rebirth":true
+  }
+}
+</programlisting>
+<para>Dit commando zorgt er voor dat alle NBIRTH en DBIRTH berichten opnieuw
+verzonden worden alsof de computer net is opgestart. Dit kan nuttig zijn na
+een herstart van de bms applicatie zelf zodat de juiste nodes informatie weer
+beschikbaar is.</para>
+</sect1>
+
+<sect1 id="payloadfermdata">
+<title>Netwerk payload data formaat voor vergisting controllers</title>
+<para>
+Dit is het meest uitgebreide formaat wat getoond is. Indien er bijvoorbeeld geen
+chiller aanwezig is, dan wordt een `null' gestuurd in plaats van een json blok
+met gegevens. De werkelijke uitvoering van de hardware en de configuratie daarvan
+bepaald dus het uiteindelijke payload formaat.
+</para>
+<programlisting>
+{
+  "uuid": "48c9ae27-3f58-41c9-ae4b-1d57b249c45a",
+  "alias": "unit1",
+  "product": {
+    "uuid": "1eb0c7bf-bf06-491c-a086-ac5478d521b9",
+    "code": "CB0001",
+    "name": "Hoppy Housebeer"
+  },
+  "air": {
+    "address": "70d60411-3ec8-40ab-998a-81fead83025f",
+    "state": "OK",
+    "temperature": 21.562
+  },
+  "beer": {
+    "address": "8ec36f9d-f382-4e32-a47f-732642e1018d",
+    "state": "OK",
+    "temperature": 22.125
+  },
+  "chiller": {
+    "address": "e81265b8-07f7-4b22-96c1-6f55a4b66a83",
+    "state": "OK",
+    "temperature": 12.437
+  },
+  "heater": {
+    "address": "d2f2d6bc-4d12-4852-9462-95f4c2476034",
+    "state": 0,
+    "usage": 10710793
+  },
+  "cooler": {
+    "address": "a9f30140-812c-4ec1-9e98-3a9d47deff7c",
+    "state": 0,
+    "usage": 920504
+  },
+  "fan": {
+    "address": "ae9f9887-8209-4810-9f58-ddfb34ee142f",
+    "state": 100,
+    "usage": 62889739
+  },
+  "light": {
+    "address": "cc6353cf-9c97-41b9-b6cf-00cea312e478",
+    "state": 0,
+    "usage": 29647290
+  },
+  "door": {
+    "address": "ad8746d1-0549-485a-a215-41e5cdde9e75",
+    "state": 1
+  },
+  "psu": {
+    "address": "e1bb7182-883d-4977-a1c0-76e214072fc5",
+    "state": 1
+  },
+  "stage": "PRIMARY",
+  "mode": "BEER",
+  "setpoint": {
+    "low": 21.0,
+    "high": 21.0
+  },
+  "webcam": {
+    "url":"https://the.webcamserver.com:8090/?action=stream",
+    "light": 1
+  },
+  "alarm": 0,
+  "profile": {
+    "uuid": "c93ad1bb-0446-4788-9c43-83990c5f8b82",
+    "name": "Witbier methode Cellis",
+    "state": "OFF",
+    "percent": 0,
+    "inittemp": {
+      "low": 17.9,
+      "high": 18.1
+    },
+    "fridgemode": 0,
+    "steps": [
+      {
+        "resttime": 2,
+        "steptime": 0,
+        "target": {
+          "low": 18.0,
+          "high": 18.0
+        },
+        "fridgemode": 0
+      },
+      {
+        "resttime": 0,
+        "steptime": 24,
+        "target": {
+          "low": 18.0,
+          "high": 22.0
+        },
+        "fridgemode": 0
+      },
+      {
+        "resttime": 48,
+        "steptime": 96,
+        "target": {
+          "low": 26.0,
+          "high": 26.0
+        },
+        "fridgemode": 0
+      }
+    ]
+  }
+}
+</programlisting>
+<para>Temperature states can be: OK, MISSING or ERROR.<para>
+<para>The general `mode' can be: OFF, NONE, FRIDGE, BEER or PROFILE.</para>
+<para>The profile `state' can be: OFF, PAUSE, RUN, DONE or ABORT.</para>
+</sect1>
+
+
+<sect1 id="payloadfermlog">
+<title>Netwerk payload log formaat voor vergisting controllers</title>
+<para>
+Dit is het meest uitgebreide formaat wat getoond is. Indien er bijvoorbeeld geen
+chiller aanwezig is, dan wordt geen data hiervoor verstuurd.
+De werkelijke uitvoering van de hardware en de configuratie daarvan
+bepaald dus het uiteindelijke payload formaat.
+</para>
+<programlisting>
+{
+  "timestamp": 1532201089,
+  "seq": 0,
+  "metric": {
+    "product": {
+      "uuid": "1eb0c7bf-bf06-491c-a086-ac5478d521b9",
+      "code": "CB0001",
+      "name": "Hoppy Housebeer"     
+    },
+    "stage": "PRIMARY",
+    "mode": "BEER",
+    "temperature": {
+      "air": 20.125,
+      "beer": 20.062,
+      "chiller": -3.000,
+      "room": 20.1
+    },
+    "setpoint": {
+      "low": 19.8,
+      "high": 20.1
+    },
+    "heater": {
+      "power": 100,
+      "usage": 1234
+    },
+    "cooler": {
+      "power": 0,
+      "usage": 27273
+    },
+    "fan": {
+      "power": 100,
+      "usage": 8273772
+    },
+    "sg": 1.023,
+    "event": "Something to mark",
+    "fermenter_uuid": "48c9ae27-3f58-41c9-ae4b-1d57b249c45a"
+  }
+}
+</programlisting>
+<para>
+De ontvangen vergisting log gegevens worden niet opgeslagen in de SQL database
+maar in platte tekst bestanden. Hierdoor is de gelogde informatie sneller toegankelijk.
+Ieder brouw product heeft zijn eigen bestand.
+De bestanden staan in <code>www/logs/fermentation/</code>.
+De bestandsnamen zijn <code>product_code\ product_name.log</code>.
+Het interne formaat is:</para>
+<programlisting>
+   2014-11-15 18:39,BEER,PRIMARY,20.312,19.750,-1.500,20.5,18.6,18.8,35,12345,0,67890,Whatsup,Fermenter
+            |         |     |      |      |      |      |    |    |   |   |   |   |      |        |
+ 0 datetime +         |     |      |      |      |      |    |    |   |   |   |   |      |        |
+ 1 werkwijze ---------+     |      |      |      |      |    |    |   |   |   |   |      |        |
+ 2 vergisting fase ---------+      |      |      |      |    |    |   |   |   |   |      |        |
+ 3 temperatuur lucht --------------+      |      |      |    |    |   |   |   |   |      |        |
+ 4 temperatuur bier ----------------------+      |      |    |    |   |   |   |   |      |        |
+ 5 temperatuur koeler ---------------------------+      |    |    |   |   |   |   |      |        |
+ 6 temperatuur ruimte ----------------------------------+    |    |   |   |   |   |      |        |
+ 7 instelwaarde laag ----------------------------------------+    |   |   |   |   |      |        |
+ 8 instelwaarde hoog ---------------------------------------------+   |   |   |   |      |        |
+ 9 verwarming vermogen -----------------------------------------------+   |   |   |      |        |
+10 verwarming verbruik ---------------------------------------------------+   |   |      |        |
+11 koeler vermogen -----------------------------------------------------------+   |      |        |
+12 koeler verbruik ---------------------------------------------------------------+      |        |
+13 gebeurtenis --------------------------------------------------------------------------+        |
+14 vergister uuid --------------------------------------------------------------------------------+
+</programlisting>
+</sect1>
+
+
+</chapter>
--- a/doc/bms.sgml	Sun May 12 19:24:34 2019 +0200
+++ b/doc/bms.sgml	Sun May 12 19:41:18 2019 +0200
@@ -2,6 +2,11 @@
 <!ENTITY chapter1 SYSTEM "bms-ch1.sgml">
 <!ENTITY chapter2 SYSTEM "bms-ch2.sgml">
 <!ENTITY chapter3 SYSTEM "bms-ch3.sgml">
+<!ENTITY chapter4 SYSTEM "bms-ch4.sgml">
+<!ENTITY chapter5 SYSTEM "bms-ch5.sgml">
+<!ENTITY chapter6 SYSTEM "bms-ch6.sgml">
+<!ENTITY chapter7 SYSTEM "bms-ch7.sgml">
+<!ENTITY chapter8 SYSTEM "bms-ch8.sgml">
 ]>
 
 <book lang="nl">
@@ -30,5 +35,10 @@
 &chapter1;
 &chapter2;
 &chapter3;
+&chapter4;
+&chapter5;
+&chapter6;
+&chapter7;
+&chapter8;
 
 </book>
--- a/www/Makefile	Sun May 12 19:24:34 2019 +0200
+++ b/www/Makefile	Sun May 12 19:41:18 2019 +0200
@@ -13,11 +13,11 @@
 		  mon_brewer.php mon_fermenter.php mon_node.php \
 		  prod_archive_code.php prod_archive_date.php prod_archive_name.php prod_beerxml.php \
 		  prod_duplicate.php prod_edit.php prod_export.php prod_forum.php prod_impbrew.php \
-		  prod_inprod.php prod_new.php prod_print.php prod_reduce.php prod_torecipe.php \
+		  prod_inprod.php prod_new.php prod_print.php prod_torecipe.php \
 		  profile_fermentation.php profile_mash.php profile_setup.php profile_styles.php \
 		  profile_water.php \
-		  rec_beerxml.php rec_duplicate.php rec_edit.php rec_export.php rec_import.php \
-		  rec_main.php rec_new.php rec_print.php rec_toproduct.php \
+		  rec_beerxml.php rec_duplicate.php rec_edit.php rec_export.php rec_forum.php \
+		  rec_import.php rec_main.php rec_new.php rec_print.php rec_toproduct.php \
 		  upl_brew.php upl_fermentables.php upl_hops.php upl_miscs.php upl_recipe.php \
 		  upl_styles.php upl_yeasts.php version.php
 SUB		= version.php.in images/* css/* jqwidgets/* jqwidgets/styles/* \
--- a/www/crontasks.php	Sun May 12 19:24:34 2019 +0200
+++ b/www/crontasks.php	Sun May 12 19:41:18 2019 +0200
@@ -12,28 +12,552 @@
 mysqli_set_charset($connect, "utf8" );
 syslog(LOG_NOTICE, "crontasks.php started");
 
+
+
+/*
+ * Upgrade inventory_reduced value from old boolean to tiny integer value.
+ */
+$query = "UPDATE products SET inventory_reduced=stage WHERE inventory_reduced = 1";
+$result = mysqli_query($connect, $query);
+$changed = mysqli_affected_rows($connect);
+if ($changed > 0) {
+        syslog(LOG_NOTICE, "Updated ".$changed." products to new inventory_reduced value");
+}
+
+
+
+/*
+ * Automatic reduce inventory depending on the production stage.
+ *  Stage 3+, primary, reduce sugars(0-mash, 1-boil), hops(0-mash, 1-fwh, 2-boil, 3-aroma, 4-whirlpool), miscs(0-starter, 1-mash, 2-boil)
+ *  Stage 4+, Secondary, reduce sugars(2-fermention), yeasts(0-Primary), miscs(3-primary)
+ *  Stage 5+, Tertiary, reduce yeasts(1-Secondary)
+ *  Stage 6+, packaged, reduce sugars(3-lagering), hops(5-dry-hop), yeasts(2-Tertiary), miscs(4-secondary)
+ *  Stage 7+, carbonatiom, reduce sugars(4-bottle), yeasts(3-Bottle), miscs(5-bottling)
+ */
+$query = "SELECT * FROM products WHERE inventory_reduced < stage";
+$result = mysqli_query($connect, $query);
+while ($row = mysqli_fetch_array($result, MYSQLI_ASSOC)) {
+
+    $savethis = 0;
+
+    /*
+     * If the brew is done, reduce the used ingredients.
+     */
+    if (($row['stage'] >= 3) && ($row['inventory_reduced'] < 3)) {
+	syslog(LOG_NOTICE, "Reduce brew inventory from " . $row['code'] . " " . $row['name']);
+
+	$fermentables = json_decode($row['json_fermentables'], true);
+        for ($i = 0; $i < count($fermentables); $i++) {
+	    if ($fermentables[$i]['f_added'] <= 1) {	// Mash, Boil
+		$sql2  = "UPDATE inventory_fermentables SET inventory = inventory - " . $fermentables[$i]['f_amount'];
+                $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_name']);
+                $sql2 .= "' AND origin='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_origin']);
+                $sql2 .= "' AND supplier='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_supplier']);
+                $sql2 .= "' AND inventory >= " . $fermentables[$i]['f_amount'];
+                $sql2 .= " LIMIT 1;";
+                $result2 = mysqli_query($connect, $sql2);
+                $ar = mysqli_affected_rows($connect);
+		if ($ar == 1) {
+		    syslog(LOG_NOTICE, "Reduced fermentable `".$fermentables[$i]['f_name']."' from `".$fermentables[$i]['f_supplier']."' with ".$fermentables[$i]['f_amount']." kg");
+		} else if ($ar == 0) {
+		    $sql2  = "UPDATE inventory_fermentables SET inventory = 0";
+                    $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_name']);
+                    $sql2 .= "' AND origin='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_origin']);
+                    $sql2 .= "' AND supplier='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_supplier']);
+                    $sql2 .= "' AND inventory < " . $fermentables[$i]['f_amount'];
+                    $sql2 .= " LIMIT 1;";
+                    $result2 = mysqli_query($connect, $sql2);
+                    $ar = mysqli_affected_rows($connect);
+                    if ($ar == 1) {
+                	syslog(LOG_NOTICE, "Reduced fermentable `".$fermentables[$i]['f_name']."' from `".$fermentables[$i]['f_supplier']."' to 0 kg");
+                    } else if ($ar == 0) {
+                        syslog(LOG_NOTICE, "Reduce fermentable `".$fermentables[$i]['f_name']."' from `".$fermentables[$i]['f_supplier']."' failed");
+                    }
+                }
+	    }
+	}
+
+	$hops = json_decode($row['json_hops'], true);
+        for ($i = 0; $i < count($hops); $i++) {
+	    if ($hops[$i]['h_useat'] <= 4) { // Mash, FWH, Boil, Flameout, Whirlpool
+		$sql2  = "UPDATE inventory_hops SET inventory = inventory - " . $hops[$i]['h_amount'];
+                $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $hops[$i]['h_name']);
+                $sql2 .= "' AND origin='" . mysqli_real_escape_string($connect, $hops[$i]['h_origin']);
+                $sql2 .= "' AND form=" . $hops[$i]['h_form'];
+                $sql2 .= " AND inventory >= " . $hops[$i]['h_amount'] . " LIMIT 1;";
+                $result2 = mysqli_query($connect, $sql2);
+                $ar = mysqli_affected_rows($connect);
+                if ($ar == 1) {
+                    syslog(LOG_NOTICE, "Reduced hop `".$hops[$i]['h_name']."' from `".$hops[$i]['h_origin']."' with ".$hops[$i]['h_amount']." kg");
+                } else if ($ar == 0) {
+                    $sql2  = "UPDATE inventory_hops SET inventory = 0";
+                    $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $hops[$i]['h_name']);
+                    $sql2 .= "' AND origin='" . mysqli_real_escape_string($connect, $hops[$i]['h_origin']);
+                    $sql2 .= "' AND form=" . $hops[$i]['h_form'];
+                    $sql2 .= " AND inventory < " . $hops[$i]['h_amount'] . " LIMIT 1;";
+                    $result2 = mysqli_query($connect, $sql2);
+                    $ar = mysqli_affected_rows($connect);
+                    if ($ar == 1) {
+                        syslog(LOG_NOTICE, "Reduced hop `".$hops[$i]['h_name']."' from `".$hops[$i]['h_origin']."' to 0 kg");
+                    } else if ($ar == 0) {
+                        syslog(LOG_NOTICE, "Reduce hop `".$hops[$i]['h_name']."' from `".$hops[$i]['h_origin']."' failed");
+                    }
+                }
+	    }
+	}
+
+	$miscs = json_decode($row['json_miscs'], true);
+        for ($i = 0; $i < count($miscs); $i++) {
+	    if ($miscs[$i]['m_use_use'] <= 2) {	// Starter, Mash, Boil
+                $sql2  = "UPDATE inventory_miscs SET inventory = inventory - " . $miscs[$i]['m_amount'];
+                $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $miscs[$i]['m_name']);
+                $sql2 .= "' AND inventory >= " . $miscs[$i]['m_amount'] . " LIMIT 1;";
+                $result2 = mysqli_query($connect, $sql2);
+                $ar = mysqli_affected_rows($connect);
+                if ($ar == 1) {
+                    syslog(LOG_NOTICE, "Reduced misc `".$miscs[$i]['m_name']."' with ".$miscs[$i]['m_amount']);
+                } else if ($ar == 0) {
+                    $sql2  = "UPDATE inventory_miscs SET inventory = 0";
+                    $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $miscs[$i]['m_name']);
+                    $sql2 .= "' AND inventory < " . $miscs[$i]['m_amount'] . " LIMIT 1;";
+                    $result2 = mysqli_query($connect, $sql2);
+                    $ar = mysqli_affected_rows($connect);
+                    if ($ar == 1) {
+                        syslog(LOG_NOTICE, "Reduced misc `".$miscs[$i]['m_name']."' to 0");
+                    } else if ($ar == 0) {
+                        syslog(LOG_NOTICE, "Reduce misc `".$miscs[$i]['m_name']."' failed");
+                    }
+                }
+	    }
+	}
+
+	if ($row['w1_name'] != '') {
+            $sql2  = "UPDATE inventory_waters SET inventory = inventory - ".$row['w1_amount'];
+            $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $row['w1_name']);
+            $sql2 .= "' AND unlimited_stock=0 AND inventory >= ".$row['w1_amount']." LIMIT 1;";
+            $result2 = mysqli_query($connect, $sql2);
+            $ar = mysqli_affected_rows($connect);
+            if ($ar == 1) {
+                syslog(LOG_NOTICE, "Reduced water `".$row['w1_name']."' with ".$row['w1_amount']." liter");
+            } else if ($ar == 0) {
+                $sql2  = "UPDATE inventory_waters SET inventory = 0";
+                $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $row['w1_name']);
+                $sql2 .= "' AND unlimited_stock=0 AND inventory < ".$row['w1_amount']." LIMIT 1;";
+                $result2 = mysqli_query($connect, $sql2);
+                $ar = mysqli_affected_rows($connect);
+                if ($ar == 1) {
+                    syslog(LOG_NOTICE, "Reduced water `".$row['w1_name']."' to 0 liters");
+                } else {
+                    syslog(LOG_NOTICE, "Reduce water `".$row['w1_name']."' not reduced is maybe tapwater");
+                }
+            }
+        }
+	if ($row['w2_name'] != '') {
+            $sql2  = "UPDATE inventory_waters SET inventory = inventory - ".$row['w2_amount'];
+            $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $row['w2_name']);
+            $sql2 .= "' AND unlimited_stock=0 AND inventory >= ".$row['w2_amount']." LIMIT 1;";
+            $result2 = mysqli_query($connect, $sql2);
+            $ar = mysqli_affected_rows($connect);
+            if ($ar == 1) {
+                syslog(LOG_NOTICE, "Reduced water `".$row['w2_name']."' with ".$row['w2_amount']." liter");
+            } else if ($ar == 0) {
+                $sql2  = "UPDATE inventory_waters SET inventory = 0";
+                $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $row['w2_name']);
+                $sql2 .= "' AND unlimited_stock=0 AND inventory < ".$row['w2_amount']." LIMIT 1;";
+                $result2 = mysqli_query($connect, $sql2);
+                $ar = mysqli_affected_rows($connect);
+                if ($ar == 1) {
+                    syslog(LOG_NOTICE, "Reduced water `".$row['w2_name']."' to 0 liters");
+		} else {
+                    syslog(LOG_NOTICE, "Reduce water `".$row['w2_name']."' failed");
+                }
+            }
+        }
+
+	$row['inventory_reduced'] = '3';
+	$savethis = 1;
+    }
+
+
+    /*
+     * After the Primary fermentation
+     */
+    if (($row['stage'] >= 4) && ($row['inventory_reduced'] < 4)) {
+        syslog(LOG_NOTICE, "Reduce Primary inventory from " . $row['code'] . " " . $row['name']);
+
+	$fermentables = json_decode($row['json_fermentables'], true);
+        for ($i = 0; $i < count($fermentables); $i++) {
+            if ($fermentables[$i]['f_added'] == 2) {    // Fermentation
+                $sql2  = "UPDATE inventory_fermentables SET inventory = inventory - " . $fermentables[$i]['f_amount'];
+                $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_name']);
+                $sql2 .= "' AND origin='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_origin']);
+                $sql2 .= "' AND supplier='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_supplier']);
+                $sql2 .= "' AND inventory >= " . $fermentables[$i]['f_amount'];
+                $sql2 .= " LIMIT 1;";
+                $result2 = mysqli_query($connect, $sql2);
+                $ar = mysqli_affected_rows($connect);
+                if ($ar == 1) {
+                    syslog(LOG_NOTICE, "Reduced fermentable `".$fermentables[$i]['f_name']."' from `".$fermentables[$i]['f_supplier']."' with ".$fermentables[$i]['f_amount']." kg");
+                } else if ($ar == 0) {
+                    $sql2  = "UPDATE inventory_fermentables SET inventory = 0";
+                    $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_name']);
+                    $sql2 .= "' AND origin='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_origin']);
+                    $sql2 .= "' AND supplier='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_supplier']);
+                    $sql2 .= "' AND inventory < " . $fermentables[$i]['f_amount'];
+                    $sql2 .= " LIMIT 1;";
+                    $result2 = mysqli_query($connect, $sql2);
+                    $ar = mysqli_affected_rows($connect);
+                    if ($ar == 1) {
+                        syslog(LOG_NOTICE, "Reduced fermentable `".$fermentables[$i]['f_name']."' from `".$fermentables[$i]['f_supplier']."' to 0 kg");
+                    } else if ($ar == 0) {
+                        syslog(LOG_NOTICE, "Reduce fermentable `".$fermentables[$i]['f_name']."' from `".$fermentables[$i]['f_supplier']."' failed");
+                    }
+                }
+            }
+        }
+
+	$miscs = json_decode($row['json_miscs'], true);
+        for ($i = 0; $i < count($miscs); $i++) {
+            if ($miscs[$i]['m_use_use'] == 3) { // Fermentation
+                $sql2  = "UPDATE inventory_miscs SET inventory = inventory - " . $miscs[$i]['m_amount'];
+                $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $miscs[$i]['m_name']);
+                $sql2 .= "' AND inventory >= " . $miscs[$i]['m_amount'] . " LIMIT 1;";
+                $result2 = mysqli_query($connect, $sql2);
+                $ar = mysqli_affected_rows($connect);
+                if ($ar == 1) {
+                    syslog(LOG_NOTICE, "Reduced misc `".$miscs[$i]['m_name']."' with ".$miscs[$i]['m_amount']);
+                } else if ($ar == 0) {
+                    $sql2  = "UPDATE inventory_miscs SET inventory = 0";
+                    $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $miscs[$i]['m_name']);
+                    $sql2 .= "' AND inventory < " . $miscs[$i]['m_amount'] . " LIMIT 1;";
+                    $result2 = mysqli_query($connect, $sql2);
+                    $ar = mysqli_affected_rows($connect);
+                    if ($ar == 1) {
+                        syslog(LOG_NOTICE, "Reduced misc `".$miscs[$i]['m_name']."' to 0");
+                    } else if ($ar == 0) {
+                        syslog(LOG_NOTICE, "Reduce misc `".$miscs[$i]['m_name']."' failed");
+                    }
+                }
+            }
+        }
+
+	$yeasts = json_decode($row['json_yeasts'], true);
+        for ($i = 0; $i < count($yeasts); $i++) {
+	    if ($yeasts[$i]['y_use'] == 0) {	// Primary
+		$sql2  = "UPDATE inventory_yeasts SET inventory = inventory - " . $yeasts[$i]['y_amount'];
+                $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_name']);
+                $sql2 .= "' AND laboratory='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_laboratory']);
+                $sql2 .= "' AND product_id='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_product_id']);
+                $sql2 .= "' AND form=" . $yeasts[$i]['y_form'];
+                $sql2 .= " AND inventory >= " . $yeasts[$i]['y_amount'] . " LIMIT 1;";
+                $result2 = mysqli_query($connect, $sql2);
+                $ar = mysqli_affected_rows($connect);
+                if ($ar == 1) {
+                    syslog(LOG_NOTICE, "Reduced yeast `".$yeasts[$i]['y_product_id'].' '.$yeasts[$i]['y_name']."' from `".$yeasts[$i]['y_laboratory']."' with ".$yeasts[$i]['y_amount']);
+                } else if ($ar == 0) {
+                    $sql2  = "UPDATE inventory_yeasts SET inventory = 0";
+                    $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_name']);
+                    $sql2 .= "' AND laboratory='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_laboratory']);
+                    $sql2 .= "' AND product_id='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_product_id']);
+                    $sql2 .= "' AND form=" . $yeasts[$i]['y_form'];
+                    $sql2 .= " AND inventory < " . $yeasts[$i]['y_amount'] . " LIMIT 1;";
+                    $result2 = mysqli_query($connect, $sql2);
+                    $ar = mysqli_affected_rows($connect);
+                    if ($ar == 1) {
+                        syslog(LOG_NOTICE, "Reduced yeast `".$yeasts[$i]['y_product_id'].' '.$yeasts[$i]['y_name']."' from `".$yeasts[$i]['y_laboratory']."' to 0");
+                    } else if ($ar == 0) {
+                        syslog(LOG_NOTICE, "Reduce yeast `".$yeasts[$i]['y_product_id'].' '.$yeasts[$i]['y_name']."' from `".$yeasts[$i]['y_laboratory']."' failed");
+                    }
+                }
+	    }
+	}
+
+        $row['inventory_reduced'] = '4';
+	$savethis = 1;
+    }
+
+
+    /*
+     * After the Seconday fermentation
+     */
+    if (($row['stage'] >= 5) && ($row['inventory_reduced'] < 5)) {
+        syslog(LOG_NOTICE, "Reduce Secondary inventory from " . $row['code'] . " " . $row['name']);
+
+	$yeasts = json_decode($row['json_yeasts'], true);
+        for ($i = 0; $i < count($yeasts); $i++) {
+            if ($yeasts[$i]['y_use'] == 1) {    // Secondary
+                $sql2  = "UPDATE inventory_yeasts SET inventory = inventory - " . $yeasts[$i]['y_amount'];
+                $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_name']);
+                $sql2 .= "' AND laboratory='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_laboratory']);
+                $sql2 .= "' AND product_id='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_product_id']);
+                $sql2 .= "' AND form=" . $yeasts[$i]['y_form'];
+                $sql2 .= " AND inventory >= " . $yeasts[$i]['y_amount'] . " LIMIT 1;";
+                $result2 = mysqli_query($connect, $sql2);
+                $ar = mysqli_affected_rows($connect);
+                if ($ar == 1) {
+                    syslog(LOG_NOTICE, "Reduced yeast `".$yeasts[$i]['y_product_id'].' '.$yeasts[$i]['y_name']."' from `".$yeasts[$i]['y_laboratory']."' with ".$yeasts[$i]['y_amount']);
+                } else if ($ar == 0) {
+                    $sql2  = "UPDATE inventory_yeasts SET inventory = 0";
+                    $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_name']);
+                    $sql2 .= "' AND laboratory='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_laboratory']);
+                    $sql2 .= "' AND product_id='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_product_id']);
+                    $sql2 .= "' AND form=" . $yeasts[$i]['y_form'];
+                    $sql2 .= " AND inventory < " . $yeasts[$i]['y_amount'] . " LIMIT 1;";
+                    $result2 = mysqli_query($connect, $sql2);
+                    $ar = mysqli_affected_rows($connect);
+                    if ($ar == 1) {
+                        syslog(LOG_NOTICE, "Reduced yeast `".$yeasts[$i]['y_product_id'].' '.$yeasts[$i]['y_name']."' from `".$yeasts[$i]['y_laboratory']."' to 0");
+                    } else if ($ar == 0) {
+                        syslog(LOG_NOTICE, "Reduce yeast `".$yeasts[$i]['y_product_id'].' '.$yeasts[$i]['y_name']."' from `".$yeasts[$i]['y_laboratory']."' failed");
+                    }
+                }
+            }
+        }
+        $row['inventory_reduced'] = '5';
+	$savethis = 1;
+    }
+
+
+    /*
+     * After the Tertiary fermentation
+     */
+    if (($row['stage'] >= 6) && ($row['inventory_reduced'] < 6)) {
+        syslog(LOG_NOTICE, "Reduce Tertiary inventory from " . $row['code'] . " " . $row['name']);
+
+        $fermentables = json_decode($row['json_fermentables'], true);
+        for ($i = 0; $i < count($fermentables); $i++) {
+            if ($fermentables[$i]['f_added'] == 3) {    // Lagering
+                $sql2  = "UPDATE inventory_fermentables SET inventory = inventory - " . $fermentables[$i]['f_amount'];
+                $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_name']);
+                $sql2 .= "' AND origin='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_origin']);
+                $sql2 .= "' AND supplier='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_supplier']);
+                $sql2 .= "' AND inventory >= " . $fermentables[$i]['f_amount'];
+                $sql2 .= " LIMIT 1;";
+                $result2 = mysqli_query($connect, $sql2);
+                $ar = mysqli_affected_rows($connect);
+                if ($ar == 1) {
+                    syslog(LOG_NOTICE, "Reduced fermentable `".$fermentables[$i]['f_name']."' from `".$fermentables[$i]['f_supplier']."' with ".$fermentables[$i]['f_amount']." kg");
+                } else if ($ar == 0) {
+                    $sql2  = "UPDATE inventory_fermentables SET inventory = 0";
+                    $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_name']);
+                    $sql2 .= "' AND origin='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_origin']);
+                    $sql2 .= "' AND supplier='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_supplier']);
+                    $sql2 .= "' AND inventory < " . $fermentables[$i]['f_amount'];
+                    $sql2 .= " LIMIT 1;";
+                    $result2 = mysqli_query($connect, $sql2);
+                    $ar = mysqli_affected_rows($connect);
+                    if ($ar == 1) {
+                        syslog(LOG_NOTICE, "Reduced fermentable `".$fermentables[$i]['f_name']."' from `".$fermentables[$i]['f_supplier']."' to 0 kg");
+                    } else if ($ar == 0) {
+                        syslog(LOG_NOTICE, "Reduce fermentable `".$fermentables[$i]['f_name']."' from `".$fermentables[$i]['f_supplier']."' failed");
+                    }
+                }
+            }
+        }
+
+	$hops = json_decode($row['json_hops'], true);
+        for ($i = 0; $i < count($hops); $i++) {
+            if ($hops[$i]['h_useat'] == 5) { // Dry hop
+                $sql2  = "UPDATE inventory_hops SET inventory = inventory - " . $hops[$i]['h_amount'];
+                $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $hops[$i]['h_name']);
+                $sql2 .= "' AND origin='" . mysqli_real_escape_string($connect, $hops[$i]['h_origin']);
+                $sql2 .= "' AND form=" . $hops[$i]['h_form'];
+                $sql2 .= " AND inventory >= " . $hops[$i]['h_amount'] . " LIMIT 1;";
+                $result2 = mysqli_query($connect, $sql2);
+                $ar = mysqli_affected_rows($connect);
+                if ($ar == 1) {
+                    syslog(LOG_NOTICE, "Reduced hop `".$hops[$i]['h_name']."' from `".$hops[$i]['h_origin']."' with ".$hops[$i]['h_amount']." kg");
+                } else if ($ar == 0) {
+                    $sql2  = "UPDATE inventory_hops SET inventory = 0";
+                    $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $hops[$i]['h_name']);
+                    $sql2 .= "' AND origin='" . mysqli_real_escape_string($connect, $hops[$i]['h_origin']);
+                    $sql2 .= "' AND form=" . $hops[$i]['h_form'];
+                    $sql2 .= " AND inventory < " . $hops[$i]['h_amount'] . " LIMIT 1;";
+                    $result2 = mysqli_query($connect, $sql2);
+                    $ar = mysqli_affected_rows($connect);
+                    if ($ar == 1) {
+                        syslog(LOG_NOTICE, "Reduced hop `".$hops[$i]['h_name']."' from `".$hops[$i]['h_origin']."' to 0 kg");
+                    } else if ($ar == 0) {
+                        syslog(LOG_NOTICE, "Reduce hop `".$hops[$i]['h_name']."' from `".$hops[$i]['h_origin']."' failed");
+                    }
+                }
+            }
+        }
+
+	$yeasts = json_decode($row['json_yeasts'], true);
+        for ($i = 0; $i < count($yeasts); $i++) {
+            if ($yeasts[$i]['y_use'] == 2) {    // Tertiary
+                $sql2  = "UPDATE inventory_yeasts SET inventory = inventory - " . $yeasts[$i]['y_amount'];
+                $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_name']);
+                $sql2 .= "' AND laboratory='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_laboratory']);
+                $sql2 .= "' AND product_id='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_product_id']);
+                $sql2 .= "' AND form=" . $yeasts[$i]['y_form'];
+                $sql2 .= " AND inventory >= " . $yeasts[$i]['y_amount'] . " LIMIT 1;";
+                $result2 = mysqli_query($connect, $sql2);
+                $ar = mysqli_affected_rows($connect);
+                if ($ar == 1) {
+                    syslog(LOG_NOTICE, "Reduced yeast `".$yeasts[$i]['y_product_id'].' '.$yeasts[$i]['y_name']."' from `".$yeasts[$i]['y_laboratory']."' with ".$yeasts[$i]['y_amount']);
+                } else if ($ar == 0) {
+                    $sql2  = "UPDATE inventory_yeasts SET inventory = 0";
+                    $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_name']);
+                    $sql2 .= "' AND laboratory='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_laboratory']);
+                    $sql2 .= "' AND product_id='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_product_id']);
+                    $sql2 .= "' AND form=" . $yeasts[$i]['y_form'];
+                    $sql2 .= " AND inventory < " . $yeasts[$i]['y_amount'] . " LIMIT 1;";
+                    $result2 = mysqli_query($connect, $sql2);
+                    $ar = mysqli_affected_rows($connect);
+                    if ($ar == 1) {
+                        syslog(LOG_NOTICE, "Reduced yeast `".$yeasts[$i]['y_product_id'].' '.$yeasts[$i]['y_name']."' from `".$yeasts[$i]['y_laboratory']."' to 0");
+                    } else if ($ar == 0) {
+                        syslog(LOG_NOTICE, "Reduce yeast `".$yeasts[$i]['y_product_id'].' '.$yeasts[$i]['y_name']."' from `".$yeasts[$i]['y_laboratory']."' failed");
+                    }
+                }
+            }
+        }
+
+	$miscs = json_decode($row['json_miscs'], true);
+        for ($i = 0; $i < count($miscs); $i++) {
+            if ($miscs[$i]['m_use_use'] == 4) { // Secondary or Tertiary
+                $sql2  = "UPDATE inventory_miscs SET inventory = inventory - " . $miscs[$i]['m_amount'];
+                $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $miscs[$i]['m_name']);
+                $sql2 .= "' AND inventory >= " . $miscs[$i]['m_amount'] . " LIMIT 1;";
+                $result2 = mysqli_query($connect, $sql2);
+                $ar = mysqli_affected_rows($connect);
+                if ($ar == 1) {
+                    syslog(LOG_NOTICE, "Reduced misc `".$miscs[$i]['m_name']."' with ".$miscs[$i]['m_amount']);
+                } else if ($ar == 0) {
+                    $sql2  = "UPDATE inventory_miscs SET inventory = 0";
+                    $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $miscs[$i]['m_name']);
+                    $sql2 .= "' AND inventory < " . $miscs[$i]['m_amount'] . " LIMIT 1;";
+                    $result2 = mysqli_query($connect, $sql2);
+                    $ar = mysqli_affected_rows($connect);
+                    if ($ar == 1) {
+                        syslog(LOG_NOTICE, "Reduced misc `".$miscs[$i]['m_name']."' to 0");
+                    } else if ($ar == 0) {
+                        syslog(LOG_NOTICE, "Reduce misc `".$miscs[$i]['m_name']."' failed");
+                    }
+                }
+            }
+        }
+
+        $row['inventory_reduced'] = '6';
+	$savethis = 1;
+    }
+
+
+    /*
+     * After packaging
+     *  reduce sugars(4-bottle), yeasts(3-Bottle), miscs(5-bottling)
+     */
+    if (($row['stage'] >= 7) && ($row['inventory_reduced'] < 7)) {
+        syslog(LOG_NOTICE, "Reduce Packaging inventory from " . $row['code'] . " " . $row['name']);
+
+	// Bottle sugar, how?
+
+	$yeasts = json_decode($row['json_yeasts'], true);
+        for ($i = 0; $i < count($yeasts); $i++) {
+            if ($yeasts[$i]['y_use'] == 3) {    // Bottle
+                $sql2  = "UPDATE inventory_yeasts SET inventory = inventory - " . $yeasts[$i]['y_amount'];
+                $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_name']);
+                $sql2 .= "' AND laboratory='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_laboratory']);
+                $sql2 .= "' AND product_id='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_product_id']);
+                $sql2 .= "' AND form=" . $yeasts[$i]['y_form'];
+                $sql2 .= " AND inventory >= " . $yeasts[$i]['y_amount'] . " LIMIT 1;";
+                $result2 = mysqli_query($connect, $sql2);
+                $ar = mysqli_affected_rows($connect);
+                if ($ar == 1) {
+                    syslog(LOG_NOTICE, "Reduced yeast `".$yeasts[$i]['y_product_id'].' '.$yeasts[$i]['y_name']."' from `".$yeasts[$i]['y_laboratory']."' with ".$yeasts[$i]['y_amount']);
+                } else if ($ar == 0) {
+                    $sql2  = "UPDATE inventory_yeasts SET inventory = 0";
+                    $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_name']);
+                    $sql2 .= "' AND laboratory='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_laboratory']);
+                    $sql2 .= "' AND product_id='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_product_id']);
+                    $sql2 .= "' AND form=" . $yeasts[$i]['y_form'];
+                    $sql2 .= " AND inventory < " . $yeasts[$i]['y_amount'] . " LIMIT 1;";
+                    $result2 = mysqli_query($connect, $sql2);
+                    $ar = mysqli_affected_rows($connect);
+                    if ($ar == 1) {
+                        syslog(LOG_NOTICE, "Reduced yeast `".$yeasts[$i]['y_product_id'].' '.$yeasts[$i]['y_name']."' from `".$yeasts[$i]['y_laboratory']."' to 0");
+                    } else if ($ar == 0) {
+                        syslog(LOG_NOTICE, "Reduce yeast `".$yeasts[$i]['y_product_id'].' '.$yeasts[$i]['y_name']."' from `".$yeasts[$i]['y_laboratory']."' failed");
+                    }
+                }
+            }
+        }
+
+	$miscs = json_decode($row['json_miscs'], true);
+        for ($i = 0; $i < count($miscs); $i++) {
+            if ($miscs[$i]['m_use_use'] == 5) { // Bottle
+                $sql2  = "UPDATE inventory_miscs SET inventory = inventory - " . $miscs[$i]['m_amount'];
+                $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $miscs[$i]['m_name']);
+                $sql2 .= "' AND inventory >= " . $miscs[$i]['m_amount'] . " LIMIT 1;";
+                $result2 = mysqli_query($connect, $sql2);
+                $ar = mysqli_affected_rows($connect);
+                if ($ar == 1) {
+                    syslog(LOG_NOTICE, "Reduced misc `".$miscs[$i]['m_name']."' with ".$miscs[$i]['m_amount']);
+                } else if ($ar == 0) {
+                    $sql2  = "UPDATE inventory_miscs SET inventory = 0";
+                    $sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $miscs[$i]['m_name']);
+                    $sql2 .= "' AND inventory < " . $miscs[$i]['m_amount'] . " LIMIT 1;";
+                    $result2 = mysqli_query($connect, $sql2);
+                    $ar = mysqli_affected_rows($connect);
+                    if ($ar == 1) {
+                        syslog(LOG_NOTICE, "Reduced misc `".$miscs[$i]['m_name']."' to 0");
+                    } else if ($ar == 0) {
+                        syslog(LOG_NOTICE, "Reduce misc `".$miscs[$i]['m_name']."' failed");
+                    }
+                }
+            }
+        }
+
+        $row['inventory_reduced'] = $row['stage'];
+	$savethis = 1;
+    }
+
+    /*
+     * Save only if something was reduced.
+     */
+    if ($savethis == 1) {
+    	$sql2 = "UPDATE products SET inventory_reduced=".$row['inventory_reduced']." WHERE uuid = '" . $row['uuid'] . "';";
+//    	syslog(LOG_NOTICE, $sql2);
+    	$result2 = mysqli_query($connect, $sql2);
+    	$ar = mysqli_affected_rows($connect);
+	if ($ar != 1) {
+    	    syslog(LOG_NOTICE, $sql2." error, affected rows: ".$ar);
+	}
+    }
+}
+
+
+
+/*
+ * Update stages after packaging depending on the age.
+ */
 $query = "UPDATE products SET stage=7 WHERE stage = 6 AND DATEDIFF(CURDATE(), package_date) > 0";
 $result = mysqli_query($connect, $query);
 $changed = mysqli_affected_rows($connect);
 if ($changed > 0) {
-	syslog(LOG_NOTICE, "Updated ".$changed." products to stage 7");
+	syslog(LOG_NOTICE, "Updated ".$changed." products to stage 7 (Carbonation)");
 }
 
 $query = "UPDATE products SET stage=8 WHERE stage = 7 AND DATEDIFF(CURDATE(), package_date) > 13";
 $result = mysqli_query($connect, $query);
 $changed = mysqli_affected_rows($connect);
 if ($changed > 0) {
-	syslog(LOG_NOTICE, "Updated ".$changed." products to stage 8");
+	syslog(LOG_NOTICE, "Updated ".$changed." products to stage 8 (Mature)");
 }
 
 $query = "UPDATE products SET stage=9 WHERE stage = 8 AND DATEDIFF(CURDATE(), package_date) > 41";
 $result = mysqli_query($connect, $query);
 $changed = mysqli_affected_rows($connect);
 if ($changed > 0) {
-	syslog(LOG_NOTICE, "Updated ".$changed." products to stage 9");
+	syslog(LOG_NOTICE, "Updated ".$changed." products to stage 9 (Taste)");
 }
 
 
+/*
+ * Check fementation logs.
+ */
 $query = "SELECT record,code,name,log_brew,log_fermentation FROM products;";
 $result = mysqli_query($connect, $query);
 while ($row = mysqli_fetch_array($result, MYSQLI_ASSOC)) {
--- a/www/includes/db_product.php	Sun May 12 19:24:34 2019 +0200
+++ b/www/includes/db_product.php	Sun May 12 19:41:18 2019 +0200
@@ -688,29 +688,29 @@
 		$brew .= ',"sparge_acid_type":' . $row['sparge_acid_type'];
 		$brew .= ',"sparge_acid_perc":' . floatval($row['sparge_acid_perc']);
 		$brew .= ',"sparge_acid_amount":' . floatval($row['sparge_acid_amount']);
-		$brew .= ',"mash_ph":' . $row['mash_ph'];
-		$brew .= ',"mash_name":"' . $row['mash_name'];
+		$brew .= ',"mash_ph":' . floatval($row['mash_ph']);
+		$brew .= ',"mash_name":"' . str_replace($escapers, $replacements, $row['mash_name']);
 		$brew .= '","calc_acid":' . $row['calc_acid'];
 		$brew .= ',"w1_name":"' . str_replace($escapers, $replacements, $row['w1_name']);
-		$brew .= '","w1_amount":' . $row['w1_amount'];
-		$brew .= ',"w1_calcium":' . $row['w1_calcium'];
-		$brew .= ',"w1_sulfate":' . $row['w1_sulfate'];
-		$brew .= ',"w1_chloride":' . $row['w1_chloride'];
-		$brew .= ',"w1_sodium":' . $row['w1_sodium'];
-		$brew .= ',"w1_magnesium":' . $row['w1_magnesium'];
-		$brew .= ',"w1_total_alkalinity":' . $row['w1_total_alkalinity'];
-		$brew .= ',"w1_ph":' . $row['w1_ph'];
-		$brew .= ',"w1_cost":' . $row['w1_cost'];
+		$brew .= '","w1_amount":' . floatval($row['w1_amount']);
+		$brew .= ',"w1_calcium":' . floatval($row['w1_calcium']);
+		$brew .= ',"w1_sulfate":' . floatval($row['w1_sulfate']);
+		$brew .= ',"w1_chloride":' . floatval($row['w1_chloride']);
+		$brew .= ',"w1_sodium":' . floatval($row['w1_sodium']);
+		$brew .= ',"w1_magnesium":' . floatval($row['w1_magnesium']);
+		$brew .= ',"w1_total_alkalinity":' . floatval($row['w1_total_alkalinity']);
+		$brew .= ',"w1_ph":' . floatval($row['w1_ph']);
+		$brew .= ',"w1_cost":' . floatval($row['w1_cost']);
 		$brew .= ',"w2_name":"' . str_replace($escapers, $replacements, $row['w2_name']);
-		$brew .= '","w2_amount":' . $row['w2_amount'];
-		$brew .= ',"w2_calcium":' . $row['w2_calcium'];
-		$brew .= ',"w2_sulfate":' . $row['w2_sulfate'];
-		$brew .= ',"w2_chloride":' . $row['w2_chloride'];
-		$brew .= ',"w2_sodium":' . $row['w2_sodium'];
-		$brew .= ',"w2_magnesium":' . $row['w2_magnesium'];
-		$brew .= ',"w2_total_alkalinity":' . $row['w2_total_alkalinity'];
-		$brew .= ',"w2_ph":' . $row['w2_ph'];
-		$brew .= ',"w2_cost":' . $row['w2_cost'];
+		$brew .= '","w2_amount":' . floatval($row['w2_amount']);
+		$brew .= ',"w2_calcium":' . floatval($row['w2_calcium']);
+		$brew .= ',"w2_sulfate":' . floatval($row['w2_sulfate']);
+		$brew .= ',"w2_chloride":' . floatval($row['w2_chloride']);
+		$brew .= ',"w2_sodium":' . floatval($row['w2_sodium']);
+		$brew .= ',"w2_magnesium":' . floatval($row['w2_magnesium']);
+		$brew .= ',"w2_total_alkalinity":' . floatval($row['w2_total_alkalinity']);
+		$brew .= ',"w2_ph":' . floatval($row['w2_ph']);
+		$brew .= ',"w2_cost":' . floatval($row['w2_cost']);
 		$brew .= ',"wa_acid_name":' . $row['wa_acid_name'];
 		$brew .= ',"wa_acid_perc":' . $row['wa_acid_perc'];
 		$brew .= ',"wa_base_name":' . $row['wa_base_name'];
@@ -784,6 +784,8 @@
 			for ($i = 0; $i < count($yeasts); $i++) {
 				$yeasts[$i]['y_inventory'] = 0;   // Not in stock
 				$yeasts[$i]['y_avail'] = 0;       // Ingredient not in db
+				if (! isset($yeasts[$i]['y_tolerance']))
+                                        $yeasts[$i]['y_tolerance'] = 0;
 				$sql2 = "SELECT inventory,tolerance FROM inventory_yeasts ";
 				$sql2 .= "WHERE name='".str_replace($rescapers, $rreplacements, $yeasts[$i]['y_name'])."' AND";
 				$sql2 .= " form='".str_replace($rescapers, $rreplacements, $yeasts[$i]['y_form'])."' AND";
--- a/www/includes/db_recipes.php	Sun May 12 19:24:34 2019 +0200
+++ b/www/includes/db_recipes.php	Sun May 12 19:41:18 2019 +0200
@@ -32,7 +32,7 @@
 		$uuid = str_replace("\n", "", file_get_contents('/proc/sys/kernel/random/uuid'));
 		$sql .= "uuid='" . $uuid;
 	}
-	($_POST['locked'] == 'true') ? $sql .= "', locked='1" : $sql .= "', locked='0";
+	$sql .= "', locked='" . $_POST['locked'];
 	$sql .= "', st_name='" . mysqli_real_escape_string($connect, $_POST['st_name']);
 	$sql .= "', st_letter='" . mysqli_real_escape_string($connect, $_POST['st_letter']);
 	$sql .= "', st_guide='" . mysqli_real_escape_string($connect, $_POST['st_guide']);
@@ -75,7 +75,7 @@
 	$sql .= "', sparge_acid_amount='" . $_POST['sparge_acid_amount'];
 	$sql .= "', mash_ph='" . $_POST['mash_ph'];
 	$sql .= "', mash_name='" . $_POST['mash_name'];
-	($_POST['calc_acid'] == 'true') ? $sql .= "', calc_acid='1" : $sql .= "', calc_acid='0";
+	$sql .= "', calc_acid='" . $_POST['calc_acid'];
 	if (isset($_POST['w1_name'])) {
 		$sql .= "', w1_name='" . mysqli_real_escape_string($connect, $_POST['w1_name']);
 		$sql .= "', w1_amount='" . $_POST['w1_amount'];
@@ -103,7 +103,7 @@
 	$sql .= "', wa_acid_name='" . $_POST['wa_acid_name'];
 	$sql .= "', wa_acid_perc='" . $_POST['wa_acid_perc'];
 	$sql .= "', wa_base_name='" . $_POST['wa_base_name'];
-	syslog(LOG_NOTICE, $sql);
+	//syslog(LOG_NOTICE, $sql);
 
 	$fermentables = '[';
 	$comma = FALSE;
@@ -202,6 +202,7 @@
 	$comma = FALSE;
 	if (isset($_POST['yeasts'])) {
 		$array = $_POST['yeasts'];
+syslog(LOG_NOTICE, $_POST['yeasts']);
 		foreach($array as $key => $item) {
 			if ($comma)
 				$yeasts .= ',';
@@ -221,7 +222,7 @@
 			$yeast .= ',"y_inventory":' . $item['y_inventory'];
 			$yeast .= ',"y_use":' . $item['y_use'];
 			$yeast .= ',"y_cost":' . $item['y_cost'] . '}';
-			//syslog(LOG_NOTICE, $yeast);
+			syslog(LOG_NOTICE, $yeast);
 			$yeasts .= $yeast;
 		}
 	}
@@ -247,7 +248,7 @@
 			$mash .= ',"ramp_time":' . $item['ramp_time'];
 			$mash .= ',"end_temp":' . $item['end_temp'] . '}';
 			$mashs .= $mash;
-			syslog(LOG_NOTICE, $mash);
+			//syslog(LOG_NOTICE, $mash);
 		}
 	}
 	$mashs .= ']';
@@ -303,7 +304,7 @@
 		}
 		$comma = TRUE;
 		$recipes .= '{"record":' . $row['record'];
-		$recipes .= ',"uuid":"' . $row['uuid'];
+		$recipes .= ',"uuid":"' . str_replace($escapers, $replacements, $row['uuid']);
 		$recipes .= '","locked":' . $row['locked'];
 		$recipes .=  ',"st_guide":"' . str_replace($escapers, $replacements, $row['st_guide']);
 		$recipes .= '","st_letter":"' . str_replace($escapers, $replacements, $row['st_letter']);
@@ -338,38 +339,38 @@
 		$recipes .= ',"color_method":' . $row['color_method'];
 		$recipes .= ',"est_ibu":' . floatval($row['est_ibu']);
 		$recipes .= ',"ibu_method":' . $row['ibu_method'];
-		$recipes .= ',"sparge_temp":' . $row['sparge_temp'];
-		$recipes .= ',"sparge_ph":' . $row['sparge_ph'];
-		$recipes .= ',"sparge_volume":' . $row['sparge_volume'];
+		$recipes .= ',"sparge_temp":' . floatval($row['sparge_temp']);
+		$recipes .= ',"sparge_ph":' . floatval($row['sparge_ph']);
+		$recipes .= ',"sparge_volume":' . floatval($row['sparge_volume']);
 		$recipes .= ',"sparge_source":"' . $row['sparge_source'];
 		$recipes .= '","sparge_acid_type":' . $row['sparge_acid_type'];
-		$recipes .= ',"sparge_acid_perc":' . $row['sparge_acid_perc'];
-		$recipes .= ',"sparge_acid_amount":' . $row['sparge_acid_amount'];
-		$recipes .= ',"mash_ph":' . $row['mash_ph'];
-		$recipes .= ',"mash_name":"' . $row['mash_name'];
+		$recipes .= ',"sparge_acid_perc":' . floatval($row['sparge_acid_perc']);
+		$recipes .= ',"sparge_acid_amount":' . floatval($row['sparge_acid_amount']);
+		$recipes .= ',"mash_ph":' . floatval($row['mash_ph']);
+		$recipes .= ',"mash_name":"' . str_replace($escapers, $replacements, $row['mash_name']);
 		$recipes .= '","calc_acid":' . $row['calc_acid'];
 		$recipes .= ',"w1_name":"' . str_replace($escapers, $replacements, $row['w1_name']);
-		$recipes .= '","w1_amount":' . $row['w1_amount'];
-		$recipes .= ',"w1_calcium":' . $row['w1_calcium'];
-		$recipes .= ',"w1_sulfate":' . $row['w1_sulfate'];
-		$recipes .= ',"w1_chloride":' . $row['w1_chloride'];
-		$recipes .= ',"w1_sodium":' . $row['w1_sodium'];
-		$recipes .= ',"w1_magnesium":' . $row['w1_magnesium'];
-		$recipes .= ',"w1_total_alkalinity":' . $row['w1_total_alkalinity'];
-		$recipes .= ',"w1_ph":' . $row['w1_ph'];
-		$recipes .= ',"w1_cost":' . $row['w1_cost'];
+		$recipes .= '","w1_amount":' . floatval($row['w1_amount']);
+		$recipes .= ',"w1_calcium":' . floatval($row['w1_calcium']);
+		$recipes .= ',"w1_sulfate":' . floatval($row['w1_sulfate']);
+		$recipes .= ',"w1_chloride":' . floatval($row['w1_chloride']);
+		$recipes .= ',"w1_sodium":' . floatval($row['w1_sodium']);
+		$recipes .= ',"w1_magnesium":' . floatval($row['w1_magnesium']);
+		$recipes .= ',"w1_total_alkalinity":' . floatval($row['w1_total_alkalinity']);
+		$recipes .= ',"w1_ph":' . floatval($row['w1_ph']);
+		$recipes .= ',"w1_cost":' . floatval($row['w1_cost']);
 		$recipes .= ',"w2_name":"' . str_replace($escapers, $replacements, $row['w2_name']);
-		$recipes .= '","w2_amount":' . $row['w2_amount'];
-		$recipes .= ',"w2_calcium":' . $row['w2_calcium'];
-		$recipes .= ',"w2_sulfate":' . $row['w2_sulfate'];
-		$recipes .= ',"w2_chloride":' . $row['w2_chloride'];
-		$recipes .= ',"w2_sodium":' . $row['w2_sodium'];
-		$recipes .= ',"w2_magnesium":' . $row['w2_magnesium'];
-		$recipes .= ',"w2_total_alkalinity":' . $row['w2_total_alkalinity'];
-		$recipes .= ',"w2_ph":' . $row['w2_ph'];
-		$recipes .= ',"w2_cost":' . $row['w2_cost'];
+		$recipes .= '","w2_amount":' . floatval($row['w2_amount']);
+		$recipes .= ',"w2_calcium":' . floatval($row['w2_calcium']);
+		$recipes .= ',"w2_sulfate":' . floatval($row['w2_sulfate']);
+		$recipes .= ',"w2_chloride":' . floatval($row['w2_chloride']);
+		$recipes .= ',"w2_sodium":' . floatval($row['w2_sodium']);
+		$recipes .= ',"w2_magnesium":' . floatval($row['w2_magnesium']);
+		$recipes .= ',"w2_total_alkalinity":' . floatval($row['w2_total_alkalinity']);
+		$recipes .= ',"w2_ph":' . floatval($row['w2_ph']);
+		$recipes .= ',"w2_cost":' . floatval($row['w2_cost']);
 		$recipes .= ',"wa_acid_name":' . $row['wa_acid_name'];
-		$recipes .= ',"wa_acid_perc":' . $row['wa_acid_perc'];
+		$recipes .= ',"wa_acid_perc":' . floatval($row['wa_acid_perc']);
 		$recipes .= ',"wa_base_name":' . $row['wa_base_name'];
 		if (isset($_GET['record'])) {
 			// Append stock information.
@@ -429,6 +430,8 @@
 			for ($i = 0; $i < count($yeasts); $i++) {
 				$yeasts[$i]['y_inventory'] = 0;   // Not in stock
 				$yeasts[$i]['y_avail'] = 0;       // Ingredient not in db
+				if (! isset($yeasts[$i]['y_tolerance']))
+					$yeasts[$i]['y_tolerance'] = 0;
 				$sql2 = "SELECT inventory,tolerance FROM inventory_yeasts ";
 				$sql2 .= "WHERE name='".str_replace($rescapers, $rreplacements, $yeasts[$i]['y_name'])."' AND";
 			       	$sql2 .= " form='".str_replace($rescapers, $rreplacements, $yeasts[$i]['y_form'])."' AND";
--- a/www/js/prod_edit.js	Sun May 12 19:24:34 2019 +0200
+++ b/www/js/prod_edit.js	Sun May 12 19:41:18 2019 +0200
@@ -57,6 +57,12 @@
 	var	pitchrate = 0.75;	// Yeast pitch rate default
 	var	initcells = 0;		// Initial yeast cell count
 
+	var	ok_fermentables = 1;	// Fermentables are in stock
+	var	ok_hops = 1;		// Hops are in stock
+	var	ok_miscs = 1;		// Miscs are in stock
+	var	ok_yeasts = 1;		// Yeasts are in stock
+	var	ok_waters = 1;		// Waters are in stock
+
         var     hop_flavour = 0;
         var     hop_aroma = 0;
         var     mash_infuse = 0;
@@ -104,18 +110,31 @@
                 theme: theme
         });
 
+	function calcSupplies() {
+
+		if (dataRecord.inventory_reduced > 6) {
+			$("#ok_pmpt").hide();
+			return;
+		}
+		if (ok_fermentables && ok_hops && ok_miscs && ok_yeasts && ok_waters)
+			$("#ok_supplies").html("<img src='images/dialog-ok-apply.png'>");
+                else
+                        $("#ok_supplies").html("<img src='images/dialog-error.png'>");
+	}
+
 	/*
 	 * All calculations that depend on changes in the fermentables,
 	 * volumes and equipments.
 	 */
 	function calcFermentables() {
 
-		console.log("calcFermentables()");
 		var sugarsf = 0;	// fermentable sugars mash + boil
 		var sugarsm = 0;	// fermentable sugars in mash
 		psugar = 0;
 		pcara = 0;
 		mashkg = 0;
+		ok_fermentables = 1;	// All is in stock.
+		ok_yeasts = 1;
 		var vol = 0;		// Volume sugars after boil
 		var addedS = 0;		// Added sugars after boil
 		var addedmass = 0;	// Added mass after boil
@@ -170,7 +189,16 @@
 				vol += (x * sugardensity + (1 - x) * 1) * row.f_amount;
 			}
 			colort += row.f_amount * ebc_to_srm(row.f_color);
+			// Check supplies.
+			if ((((dataRecord.inventory_reduced <= 2) && (row.f_added <= 1)) ||  // Mash or boil
+                             ((dataRecord.inventory_reduced <= 3) && (row.f_added == 2)) ||  // Primary
+                             ((dataRecord.inventory_reduced <= 5) && (row.f_added == 3)) ||  // Secondary or Tertiary
+		    	     ((dataRecord.inventory_reduced <= 6) && (row.f_added == 4))) && row.f_inventory < row.f_amount) {
+				ok_fermentables = 0;
+			}
 		}
+		$("#mash_kg").val(mashkg);
+		console.log("calcFermentables() supplies:"+ok_fermentables);
 		to_100 = my_100;
 		if (to_100) {
 			$("#wf_amount").jqxNumberInput({ width: 90, readOnly: true, spinButtons: false  });
@@ -221,7 +249,6 @@
 		if (dataRecord.brew_fermenter_volume > 0) {
 			var sug = sg_to_plato(ogx) * dataRecord.brew_fermenter_volume * ogx / 100;  //kg of sugar in
 			sug += addedS; //kg
-			//console.log("Contents ferm_vol:"+dataRecord.brew_fermenter_volume+" top:"+top+" vol:"+vol+" addedS:"+addedS+" addedmass:"+addedmass);
 
 			if ((dataRecord.brew_fermenter_volume * ogx + addedmass) > 0) {
 				var pt = 100 * sug / (dataRecord.brew_fermenter_volume * ogx + addedmass + top);
@@ -234,7 +261,6 @@
 				var scolor = ebc_to_color(dataRecord.brew_fermenter_color);
 				$("#bcolorf").show();
 				document.getElementById("bcolorf").style.background= scolor;
-				//console.log("OG in fermenter:"+dataRecord.brew_fermenter_sg+" color:"+dataRecord.brew_fermenter_color);
 			}
 		} else {
 			// Negative volume
@@ -256,7 +282,6 @@
 
 		// Progress bars
                 pmalts = mashkg / dataRecord.eq_mash_max * 100;
-		//console.log("mash kg: "+mashkg+" max: "+dataRecord.eq_mash_max+" perc: "+pmalts);
                 $("#perc_malts").jqxProgressBar('val', pmalts);
                 $("#perc_sugars").jqxProgressBar('val', psugar);
                 $("#perc_cara").jqxProgressBar('val', pcara);
@@ -277,7 +302,15 @@
 					initcells += (parseFloat(row.y_cells) / 1000000) * parseFloat(row.y_amount);
 			}
 			// TODO: brett in secondary ??
+			if ((((dataRecord.inventory_reduced <= 3) && (row.y_use == 0)) ||  // Primary
+                             ((dataRecord.inventory_reduced <= 4) && (row.y_use == 1)) ||  // Secondary
+                             ((dataRecord.inventory_reduced <= 5) && (row.y_use == 2)) ||  // Tertiary
+                             ((dataRecord.inventory_reduced <= 6) && (row.y_use == 3))) && // Bottle
+			     (row.y_inventory < row.y_amount)) {
+				ok_yeasts = 0;
+			}
 		}
+		calcSupplies();
 		if (svg == 0)
 			svg = 77;
 
@@ -297,7 +330,6 @@
 		// Calculate the final svg if available use the real value.
 		if ((dataRecord.stage >= 6) && (dataRecord.fg > 0.990) && (dataRecord.fg < dataRecord.brew_fermenter_sg)) {
 			svg = 100 * (dataRecord.brew_fermenter_sg - dataRecord.fg) / (dataRecord.brew_fermenter_sg - 1);
-			//console.log("real svg:"+svg);
 		}
 
 		$("#yeast_cells").val(initcells);
@@ -364,9 +396,7 @@
 		if (volume <= 0)
 			volume = dataRecord.batch_size - dataRecord.eq_trub_chiller_loss;
 
-		//console.log("getNeededYeastCells f:"+f+" volume:"+volume+" plato:"+plato+" sg:"+sg);
 		var result = pitchrate * volume * plato;
-		//console.log("getNeededYeastCells("+pitchrate+"): "+result+" billion cells");
 		return result;
 	}
 
@@ -412,6 +442,7 @@
 		if (!(rows = $('#hopGrid').jqxGrid('getrows'))) {
 			return;
 		}
+		ok_hops = 1;
 		for (var i = 0; i < rows.length; i++) {
 			var row = rows[i];
 			total_ibus += toIBU(row.h_useat, row.h_form, preboil_sg, parseFloat(dataRecord.batch_size),
@@ -423,6 +454,10 @@
 					row.h_useat, parseFloat(row.h_amount));
 			hop_aroma += hopAromaContribution(parseFloat(row.h_time), parseFloat(dataRecord.batch_size),
 					row.h_useat, parseFloat(row.h_amount));
+			if ((((dataRecord.inventory_reduced <= 2) && (row.h_useat <= 4)) ||  // Mash, FW, Boil, Aroma, Whirlpool
+                             ((dataRecord.inventory_reduced <= 6) && (row.h_useat == 5))) && // Dry-hop
+			     (row.h_inventory < row.h_amount))
+				ok_hops = 0;
 		}
 		total_ibus = Math.round(total_ibus * 10) / 10;
 		ferm_ibus = Math.round(ferm_ibus * 10) / 10;
@@ -432,7 +467,7 @@
 			hop_flavour = 100;
 		if (hop_aroma > 100)
 			hop_aroma = 100;
-		console.log("calcIBUs(): " + total_ibus + "  flavour: " + hop_flavour + "  aroma: " + hop_aroma+"  fermenter:"+ferm_ibus);
+		console.log("calcIBUs(): " + total_ibus + "  flavour: " + hop_flavour + "  aroma: " + hop_aroma+"  fermenter:"+ferm_ibus+" supplies:"+ok_hops);
 		dataRecord.est_ibu = total_ibus;
 		$('#est_ibu').val(total_ibus);
 		$('#est_ibu2').val(total_ibus);
@@ -440,6 +475,7 @@
 		$("#hop_aroma").jqxProgressBar('val', hop_aroma);
 		$("#brew_fermenter_ibu").val(ferm_ibus);
 		calcStage();
+		calcSupplies();
 	};
 
 	/*
@@ -728,7 +764,6 @@
 		var needed = getNeededYeastCells();
 		console.log("calcYeast() pitchrate:"+pitchrate+" start:"+initcells+" needed:"+needed);
 		calcSteps(dataRecord.starter_type, initcells, needed);
-		//console.log("calcYeast() pitchrate:"+pitchrate+" needed:"+needed);
 		$("#need_cells").val(needed);
 
 		$("#r1_irate").html("");
@@ -814,6 +849,27 @@
 		}
 	};
 
+	function calcMiscs() {
+
+		ok_miscs = 1;
+		var rowscount = $("#miscGrid").jqxGrid('getdatainformation').rowscount;
+
+		if (rowscount == 0)
+                        return;
+
+                for (var i = 0; i < rowscount; i++) {
+                        var row = $("#miscGrid").jqxGrid('getrowdata', i);
+			if ((((dataRecord.inventory_reduced <= 2) && (row.m_use_use <= 2)) ||  // Starter, Mash, Boil
+                             ((dataRecord.inventory_reduced <= 3) && (row.m_use_use == 3)) ||  // Primary
+                             ((dataRecord.inventory_reduced <= 5) && (row.m_use_use == 4)) ||  // Secondary, Teriary
+                             ((dataRecord.inventory_reduced <= 6) && (row.m_use_use == 5))) && // Bottle
+			     (row.m_inventory < row.m_amount)) {
+				ok_miscs = 0;
+			}
+		}
+		calcSupplies();
+	};
+
 	function adjustMiscs(factor) {
 
 		console.log("adjustMiscs("+factor+")");
@@ -904,7 +960,6 @@
 			return;
 		var c = sg_to_plato(est_mash_sg);
 		var m = sg_to_plato(parseFloat($("#brew_mash_sg").jqxNumberInput('decimal')));
-		//console.log("calcMashEfficiency() c "+ c + "  m " + m + "  in " + parseFloat($("#brew_mash_sg").jqxNumberInput('decimal')));
 		if (c > 0.5)
 			$("#brew_mash_efficiency").val(100 * m / c);
 		else
@@ -929,7 +984,6 @@
 			result = Math.round((tot / m * 100) * 10) / 10;
 		if (result < 0)
 			result = 0;
-		//console.log("calcEfficiencyBeforeBoil(): "+result);
 		$("#brew_preboil_efficiency").val(result);
 	}
 
@@ -955,7 +1009,6 @@
 			result = Math.round((tot / m * 100) * 10) / 10;
 		if (result < 0)
 			result = 0;
-		//console.log("calcEfficiencyAfterBoil(): "+result);
 		dataRecord.brew_aboil_efficiency = result;
 		$("#brew_aboil_efficiency").val(result);
 
@@ -975,7 +1028,6 @@
 	}
 
 	function setWaterAgent(name, amount) {
-		//console.log("setWaterAgent(" + name + ", " + amount + ")");
 		var rows = $('#miscGrid').jqxGrid('getrows');
 		if (amount == 0) {
 			for (var i = 0; i < rows.length; i++) {
@@ -1259,7 +1311,6 @@
 		// Einde noot.
 
 		if ($("#wa_acid_name").val() < 0 || $("#wa_acid_name").val() > 3) {
-			console.log("fix wa_acid_name");
 			$("#wa_acid_name").val(0);
 			dataRecord.wa_acid_name = 0;
 		}
@@ -1267,7 +1318,6 @@
 			last_acid = AcidTypeData[$("#wa_acid_name").val()].nl;
 
 		if ($("#wa_base_name").val() < 0 || $("#wa_base_name").val() > 3) {
-			console.log("fix wa_base_name");
 			$("#wa_base_name").val(0);
 			dataRecord.wa_base_name = 0;
 		}
@@ -1529,7 +1579,6 @@
 		else if (RA > piCLSO4_high)
 			Res = 'hoog';
 		setRangeIndicator('cl_so4', Res);
-//		console.log("low: "+piCLSO4_low+" val: "+RA+" high: "+piCLSO4_high);
 
 		$('#wb_calcium').val(Math.round(calcium * 10) / 10);
 		$('#wb_magnesium').val(Math.round(magnesium * 10) / 10);
@@ -1579,6 +1628,8 @@
 			setRangeIndicator("ph", "normaal");
 		}
 		calcSparge();
+		calcMiscs();
+		calcSupplies();
 	}
 
 	function calcSparge() {
@@ -1605,7 +1656,6 @@
 				$("#sparge_source").val(0);
 			}
 		}
-		//console.log("calcSparge() target pH: "+TargetpH+" Source: "+Source_pH+" alkalinity: "+Source_alkalinity);
 
 		// Step 1: Compute the mole fractions of carbonic (f1o), bicarbonate (f2o) and carbonate(f3o) at the water pH
 		var r1 = Math.pow(10, Source_pH - 6.38);
@@ -1784,7 +1834,6 @@
 			// This is the calculated difference in seconds
 			var timeDifference = date1_unixtime - date2_unixtime;
 			var timeDifferenceInDays = timeDifference / 60 / 60 / 24;
-			//console.log(date1+'  '+date2+' days: '+timeDifferenceInDays);
 			if (timeDifferenceInDays > 0) {			// At least one day
 				if (timeDifferenceInDays >= 42)		// 6 weeks
 					newstage = 9;			// Ready to taste
@@ -1899,16 +1948,12 @@
 			$("#primary_end_brix").jqxNumberInput({ spinButtons: false, readOnly: true, width: 90 });
 			$("#primary_end_date").jqxDateTimeInput({ disabled: true });
 		}
-		if (dataRecord.stage < 6) {	// Not yet packaged
-			$("#inventory_reduced").jqxCheckBox({ disabled : true });
-		} else {
+		if (dataRecord.stage >= 6) {	// Packaged
 			$("#secondary_temp").jqxNumberInput({ spinButtons: false, readOnly: true, width: 90 });
 			$("#secondary_end_date").jqxDateTimeInput({ disabled: true });
 			$("#tertiary_temp").jqxNumberInput({ spinButtons: false, readOnly: true, width: 90 });
 			$("#fg").jqxNumberInput({ spinButtons: false, readOnly: true, width: 90 });
 			$("#final_brix").jqxNumberInput({ spinButtons: false, readOnly: true, width: 90 });
-			if ($('#inventory_reduced').jqxCheckBox('checked'))
-				$("#inventory_reduced").jqxCheckBox({ disabled : true });
 			$("#package_date").jqxDateTimeInput({ disabled: true });
 			$("#bottle_amount").jqxNumberInput({ spinButtons: false, readOnly: true, width: 90 });
 			$("#keg_amount").jqxNumberInput({ spinButtons: false, readOnly: true, width: 90 });
@@ -2854,7 +2899,6 @@
 			$("#birth").val(dataRecord.birth);
 			$("#stage").val(StageData[dataRecord.stage].nl);
 			$("#notes").val(dataRecord.notes);
-			$("#inventory_reduced").val(dataRecord.inventory_reduced);
 			$("#locked").val(dataRecord.locked);
 			$("#eq_name").val(dataRecord.eq_name);
 			$("#eq_notes").val(dataRecord.eq_notes);
@@ -3242,10 +3286,17 @@
 				{ text: 'Voorraad Kg', datafield: 'f_inventory', width: 120, align: 'right',
 				  cellsrenderer:  function (row, columnfield, value, defaulthtml, columnproperties, rowdata) {
 					var color = '#ffffff';
-					if (value < rowdata.f_amount)
-						color = '#ff4040';
-					return  '<span style="margin: 4px; margin-top: 6px; float: right; color: ' +
-						color + ';">' +fermentableAdapter.formatNumber(value, "f3") + '</span>';
+					if (((dataRecord.inventory_reduced <= 2) && (rowdata.f_added <= 1)) ||	// Mash or boil
+					    ((dataRecord.inventory_reduced <= 3) && (rowdata.f_added == 2)) ||	// Primary
+					    ((dataRecord.inventory_reduced <= 5) && (rowdata.f_added == 3)) ||	// Secondary or Tertiary
+					    ((dataRecord.inventory_reduced <= 6) && (rowdata.f_added == 4))) {	// Bottle
+						if (value < rowdata.f_amount)
+							color = '#ff4040';
+						return  '<span style="margin: 4px; margin-top: 6px; float: right; color: ' +
+							color + ';">' +fermentableAdapter.formatNumber(value, "f3") + '</span>';
+					} else {
+						return  '<span></span>';
+					}
 				  }
 				},
                                 { text: 'Procent', datafield: 'f_percentage', width: 90, align: 'right',
@@ -3446,13 +3497,18 @@
 				},
 				{ text: 'Voorraad', datafield: 'h_inventory', width: 110, align: 'right',
 				  cellsrenderer: function (index, datafield, value, defaultvalue, column, rowdata) {
-					var color = '#ffffff';
-					if (value < rowdata.h_amount)
-						color = '#ff4040';
-					var amount = dataAdapter.formatNumber(value, "f1") + ' kg';
-					if (value < 1)
-						amount = dataAdapter.formatNumber(value * 1000, "f1") + ' gr';
-					return  '<span style="margin: 4px; margin-top: 6px; float: right; color: ' + color + ';">' + amount + '</span>';
+					if (((dataRecord.inventory_reduced <= 2) && (rowdata.h_useat <= 4)) ||  // Mash, FW, Boil, Aroma, Whirlpool
+                                            ((dataRecord.inventory_reduced <= 6) && (rowdata.h_useat == 5))) {	// Dry hop
+						var color = '#ffffff';
+						if (value < rowdata.h_amount)
+							color = '#ff4040';
+						var amount = dataAdapter.formatNumber(value, "f1") + ' kg';
+						if (value < 1)
+							amount = dataAdapter.formatNumber(value * 1000, "f1") + ' gr';
+						return  '<span style="margin: 4px; margin-top: 6px; float: right; color: '+color+';">'+amount+'</span>';
+					} else {
+						return '<span></span>';
+					}
 				  }
 				},
 				{ text: '', datafield: 'Edit', columntype: 'button', width: 100, align: 'center', cellsrenderer: function () {
@@ -3630,6 +3686,7 @@
                                 });
                         },
                         ready: function() {
+				calcMiscs();
                                 $('#jqxTabs').jqxTabs('next');
                         },
                         columns: [
@@ -3663,12 +3720,19 @@
 				},
 				{ text: 'Voorraad', datafield: 'm_inventory', width: 110, align: 'right',
 				  cellsrenderer: function (index, datafield, value, defaultvalue, column, rowdata) {
-					var vstr = rowdata.m_amount_is_weight ? "gr":"ml";
-					var color = '#ffffff';
-					if (value < rowdata.m_amount)
-						color = '#ff4040';
-					var amount = dataAdapter.formatNumber(value * 1000,"f2")+" "+vstr;
-					return  '<span style="margin: 4px; margin-top: 6px; float: right; color: ' + color + ';">' + amount + '</span>';
+					if (((dataRecord.inventory_reduced <= 2) && (rowdata.m_use_use <= 2)) ||  // Starter, Mash, Boil
+					    ((dataRecord.inventory_reduced <= 3) && (rowdata.m_use_use == 3)) ||  // Primary
+					    ((dataRecord.inventory_reduced <= 5) && (rowdata.m_use_use == 4)) ||  // Secondary, Teriary
+                                            ((dataRecord.inventory_reduced <= 6) && (rowdata.m_use_use == 5))) {  // Bottle
+						var vstr = rowdata.m_amount_is_weight ? "gr":"ml";
+						var color = '#ffffff';
+						if (value < rowdata.m_amount)
+							color = '#ff4040';
+						var amount = dataAdapter.formatNumber(value * 1000,"f2")+" "+vstr;
+						return  '<span style="margin: 4px; margin-top: 6px; float: right; color: '+color+';">'+amount+'</span>';
+					} else {
+						return '<span></span>';
+					}
 				  }
 				},
 				{ text: '', datafield: 'Edit', columntype: 'button', width: 100, align: 'center', cellsrenderer: function () {
@@ -3852,15 +3916,22 @@
 				},
 				{ text: 'Voorraad', datafield: 'y_inventory', width: 90, align: 'right',
 				  cellsrenderer: function (index, datafield, value, defaultvalue, column, rowdata) {
-					var color = '#ffffff';
-					if (value < rowdata.y_amount)
-						color = '#ff4040';
-					var amount = dataAdapter.formatNumber(value*1000, "f0")+" ml";
-					if (rowdata.y_form == 0)        // Liquid
-						amount = dataAdapter.formatNumber(value, "f0")+" pk";
-					else if (rowdata.y_form == 1)   // Dry
-						amount = dataAdapter.formatNumber(value*1000, "f1")+" gr";
-					return  '<span style="margin: 4px; margin-top: 6px; float: right; color: ' + color + ';">' + amount + '</span>';
+					if (((dataRecord.inventory_reduced <= 3) && (rowdata.y_use == 0)) ||  // Primary
+                                            ((dataRecord.inventory_reduced <= 4) && (rowdata.y_use == 1)) ||  // Secondary
+                                            ((dataRecord.inventory_reduced <= 5) && (rowdata.y_use == 2)) ||  // Tertiary
+                                            ((dataRecord.inventory_reduced <= 6) && (rowdata.y_use == 3))) {  // Bottle
+						var color = '#ffffff';
+						if (value < rowdata.y_amount)
+							color = '#ff4040';
+						var amount = dataAdapter.formatNumber(value*1000, "f0")+" ml";
+						if (rowdata.y_form == 0)        // Liquid
+							amount = dataAdapter.formatNumber(value, "f0")+" pk";
+						else if (rowdata.y_form == 1)   // Dry
+							amount = dataAdapter.formatNumber(value*1000, "f1")+" gr";
+						return  '<span style="margin: 4px; margin-top: 6px; float: right; color: '+color+';">'+amount+'</span>';
+					} else {
+						return '<span></span>';
+					}
 				  }
 				},
 				{ text: '', datafield: 'Edit', columntype: 'button', width: 90, align: 'center', cellsrenderer: function () {
@@ -4032,29 +4103,6 @@
 	$("#birth").jqxInput({ theme: theme, width: 120, height: 23 });
 	$("#stage").jqxTooltip({ content: 'De productie fase van dit product.' });
 	$("#stage").jqxInput({ theme: theme, width: 100, height: 23 });
-	$("#inventory_reduced").jqxCheckBox({ theme: theme, width: 120, height: 23 });
-	$('#inventory_reduced').on('checked', function (event) {
-		if (dataRecord.inventory_reduced == 0) {
-			saveRecord();
-			var data = "reduce=1&uuid="+dataRecord.uuid+"&record="+my_record;
-			var url = "prod_reduce.php";
-			$.ajax({
-				dataType: 'json',
-				url: url,
-				cache: false,
-				data: data,
-				type: "POST",
-				success: function (data, status, xhr) {
-					console.log("success");
-					window.location.href = my_return;
-				},
-				error: function(jqXHR, textStatus, errorThrown) {
-					console.log("error");
-					window.location.href = my_return;
-				}
-			});
-		}
-	});
 	$("#locked").jqxCheckBox({ theme: theme, width: 120, height: 23, disabled : true });
 	$('#locked').on('checked', function (event) {
 		if (dataRecord.stage >= 10) {
@@ -4208,6 +4256,8 @@
 	$("#est_color2").jqxNumberInput( Show0dec );
 	$("#est_og2").jqxTooltip({ content: 'Het geschatte begin SG van dit product.' });
 	$("#est_og2").jqxNumberInput( Show3dec );
+	$("#mash_kg").jqxTooltip({ content: 'Het gewicht van alle mouten in de maisch.' });
+	$("#mash_kg").jqxNumberInput( Show3dec );
 	$("#perc_malts").jqxProgressBar({
 		width: 300,
 		height: 23,
@@ -4622,6 +4672,7 @@
 	$("#MiscReady").jqxButton({ template: "success", width: '90px', theme: theme });
 	$("#MiscReady").click(function () {
 		$("#miscGrid").jqxGrid('sortby', 'm_use_use', 'asc');
+		calcMiscs();
 	});
 	$("#wm_name").jqxInput({ theme: theme, width: 320, height: 23 });
 	$("#wm_instock").jqxCheckBox({ theme: theme, height: 23 });
--- a/www/js/rec_edit.js	Sun May 12 19:24:34 2019 +0200
+++ b/www/js/rec_edit.js	Sun May 12 19:41:18 2019 +0200
@@ -1200,11 +1200,11 @@
 		console.log("calc.init()");
 
 		$("#calc_acid").on('checked', function (event) {
-			dataRecord.calc_acid = true;
+			dataRecord.calc_acid = 1;
 			calcWater();
 		});
 		$("#calc_acid").on('unchecked', function (event) {
-			dataRecord.calc_acid = false;
+			dataRecord.calc_acid = 0;
 			calcWater();
 		});
 		$("#w1_name").jqxDropDownList('selectItem', dataRecord.w1_name);
@@ -1384,9 +1384,11 @@
 			calcSparge();
 		});
 		$('#locked').on('checked', function (event) {
+			dataRecord.locked = 1;
 			setReadonly(true);
 		});
 		$('#locked').on('unchecked', function (event) {
+			dataRecord.locked = 0;
 			setReadonly(false);
 		});
 //		setReadonly(false);
@@ -1442,7 +1444,7 @@
 			record: my_record,
 			uuid: dataRecord.uuid,
 			name: $("#name").val(),
-			locked: $("#locked").val(),
+			locked: dataRecord.locked,
 			notes: $("#notes").val(),
 			st_name: $('#st_name').val(),
 			st_letter: $('#st_letter').val(),
@@ -1484,7 +1486,7 @@
 			sparge_acid_type: $("#sparge_acid_type").val(),
 			sparge_acid_perc: parseFloat($("#sparge_acid_perc").jqxNumberInput('decimal')),
 			sparge_acid_amount: dataRecord.sparge_acid_amount,
-			calc_acid: $("#calc_acid").val(),
+			calc_acid: dataRecord.calc_acid,
 			w1_name: $("#w1_name").val(),
 			w1_amount: parseFloat($("#w1_amount").jqxNumberInput('decimal')),
 			w1_calcium: parseFloat($("#w1_calcium").jqxNumberInput('decimal')),
@@ -1541,7 +1543,7 @@
 		datafields: [
 			{ name: 'record', type: 'number' },
 			{ name: 'uuid', type: 'string' },
-			{ name: 'locked', type: 'bool' },
+			{ name: 'locked', type: 'int' },
 			{ name: 'st_name', type: 'string' },
 			{ name: 'st_letter', type: 'string' },
 			{ name: 'st_guide', type: 'string' },
@@ -1584,7 +1586,7 @@
 			{ name: 'sparge_acid_amount', type: 'float' },
 			{ name: 'mash_ph', type: 'float' },
 			{ name: 'mash_name', type: 'string' },
-			{ name: 'calc_acid', type: 'bool' },
+			{ name: 'calc_acid', type: 'int' },
 			{ name: 'w1_name', type: 'string' },
 			{ name: 'w1_amount', type: 'float' },
 			{ name: 'w1_calcium', type: 'float' },
@@ -3734,14 +3736,7 @@
 		position: 'top'
 	});
 
-	$("#Print").jqxButton({ template: "info", width: '80px', theme: theme });
-	$("#Print").bind('click', function () {
-		saveRecord();
-		// Open print in a new tab.
-		var url="rec_print.php?record=" + my_record;
-		window.open(url);
-	});
-
+	// Buttons below
 	$("#Export").jqxButton({ template: "info", width: '80px', theme: theme });
 	$("#Export").bind('click', function () {
 		saveRecord();
--- a/www/js/rec_export.js	Sun May 12 19:24:34 2019 +0200
+++ b/www/js/rec_export.js	Sun May 12 19:41:18 2019 +0200
@@ -29,6 +29,8 @@
 	$("#jqxRadioButton1").jqxRadioButton({ theme: theme, width: 250, height: 23 });
 	$("#jqxRadioButton2").jqxRadioButton({ theme: theme, width: 250, height: 23 });
 	$("#jqxRadioButton3").jqxRadioButton({ theme: theme, width: 250, height: 23 });
+	$("#jqxRadioButton4").jqxRadioButton({ theme: theme, width: 250, height: 23 });
+        $("#jqxRadioButton5").jqxRadioButton({ theme: theme, width: 250, height: 23 });
 	$('#jqxRadioButton1').on('checked', function (event) {
 		$('#Start').jqxButton({ disabled: false });
 		choice = 1;
@@ -41,6 +43,14 @@
 		$('#Start').jqxButton({ disabled: false });
 		choice = 3;
 	});
+	$('#jqxRadioButton4').on('checked', function (event) {
+                $('#Start').jqxButton({ disabled: false });
+                choice = 4;
+        });
+        $('#jqxRadioButton5').on('checked', function (event) {
+                $('#Start').jqxButton({ disabled: false });
+                choice = 5;
+        });
 
 	$('#Start').jqxButton({ template: "success", width: '100px', theme: theme, disabled: true });
 	$('#Start').click(function () {
@@ -53,8 +63,14 @@
 		} else if (choice == 3) {
 			var url="rec_toproduct.php?record=" + my_record;
 			window.open(url);
-		}
-		// Return to the original product
-		window.location.href = my_return + "?record=" + my_record;
+		} else if (choice == 4) {
+                        var url="rec_print.php?record=" + my_record;
+                        window.open(url);
+                } else if (choice == 5) {
+                        var url="rec_forum.php?record=" + my_record;
+                        window.open(url);
+                }
+		// Return to the original recipe
+		window.location.href = "rec_edit.php?record=" + my_record + "&return=" + my_return;
 	});
 });
--- a/www/prod_edit.php	Sun May 12 19:24:34 2019 +0200
+++ b/www/prod_edit.php	Sun May 12 19:41:18 2019 +0200
@@ -45,8 +45,8 @@
         <td align="left" style="padding: 3px;"><div id="type"></div></td>
         <td style="vertical-align: top; float: right; padding: 3px;">Brouwzaal rendement:</td>
 	<td style="padding: 3px;"><div id="efficiency"></div></td>
-        <td align="right" style="vertical-align: top;">Ingredienten afgeboekt:</td>
-        <td align="left"><div id="inventory_reduced"></div></td>
+	<td style="vertical-align: top; float: right; padding: 3px;" id="ok_pmpt">Ingredienten aanwezig:</td>
+        <td align="left"><div id="ok_supplies"></div></td>
        </tr>
        <tr>
         <td style="vertical-align: top; float: right; padding: 3px;">Brouw volume:</td>
@@ -203,7 +203,8 @@
         <td style="padding: 3px;"><div style="float: left;" id="perc_sugars"></div></td>
        </tr>
        <tr>
-        <td colspan="2"></td>
+        <td style="vertical-align: top; float: right; padding: 3px;">Maisch KG:</td>
+        <td style="padding: 3px;"><div style="float: left;" id="mash_kg"></div></td>
         <td style="vertical-align: top; float: right; padding: 3px;">Percentage cara:</td>
         <td style="padding: 3px;"><div style="float: left;" id="perc_cara"></div></td>
        </tr>
--- a/www/prod_forum.php	Sun May 12 19:24:34 2019 +0200
+++ b/www/prod_forum.php	Sun May 12 19:41:18 2019 +0200
@@ -150,8 +150,8 @@
 }
 echo PHP_EOL . PHP_EOL;
 
-echo "Specerij, kruid, brouwzout etc Type grondstof  Gebruik tijdens         Hoeveel" . PHP_EOL;
-echo "------------------------------ --------------- -------------------- ----------" . PHP_EOL;
+echo "Specerij, kruid, brouwzout etc Type grondstof  Gebruik tijdens          Hoeveel" . PHP_EOL;
+echo "------------------------------ --------------- -------------------- -----------" . PHP_EOL;
 $arr = json_decode($row['json_miscs'], true);
 foreach($arr as $item) {
 	$amount  = floatval($item['m_amount']) * 1000;
@@ -163,9 +163,9 @@
         	$gebruik = sprintf("%s %d min",$miscuse[$use_use],$time);
         else
         	$gebruik = $miscuse[$use_use];
-	$hoeveel = sprintf("%.1f %s",$amount,$aiw ? "gr":"ml");
+	$hoeveel = sprintf("%.2f %s",$amount,$aiw ? "gr":"ml");
 
-	echo sprintf("%s %-15s %-20s %10s", formatstr($item['m_name'], 30), $misctype[$item['m_type']], $gebruik, $hoeveel) . PHP_EOL;
+	echo sprintf("%s %-15s %-20s %11s", formatstr($item['m_name'], 30), $misctype[$item['m_type']], $gebruik, $hoeveel) . PHP_EOL;
 }
 echo PHP_EOL . PHP_EOL;
 
--- a/www/prod_print.php	Sun May 12 19:24:34 2019 +0200
+++ b/www/prod_print.php	Sun May 12 19:41:18 2019 +0200
@@ -373,7 +373,7 @@
 			else
 				$gebruik = $miscuse[$use_use];
 
-			$hoeveel = sprintf("%.1f %s",$amount,$aiw ? "gr":"ml");
+			$hoeveel = sprintf("%.2f %s",$amount,$aiw ? "gr":"ml");
 			$this->Cell($vul,5,$name,0,0,'L',true);
 			$this->Cell(30,5,$misctype[$type],0,0,'L',true);
 			$this->Cell(30,5,$gebruik,0,0,'L',true);
--- a/www/prod_reduce.php	Sun May 12 19:24:34 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,209 +0,0 @@
-<?php
-
-require_once('config.php');
-
-#Connect to the database
-$connect = mysqli_connect(DBASE_HOST, DBASE_USER, DBASE_PASS, DBASE_NAME);
-if (! $connect) {
-	die('Connect Error (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
-}
-mysqli_set_charset($connect, "utf8" );
-
-if (isset($_POST['reduce']) && isset($_POST['uuid'])) {
-	$sql1 = "SELECT * FROM products WHERE uuid = '" . $_POST['uuid'] . "';";
-} else {
-	exit;
-}
-
-//syslog(LOG_NOTICE, $sql1);
-
-$result1 = mysqli_query($connect, $sql1) or die("SQL Error 1: " . mysqli_error($connect));
-while ($row = mysqli_fetch_array($result1, MYSQLI_ASSOC)) {
-
-	if ($row['stage'] < 6) {
-		syslog(LOG_NOTICE, "code: " . $row['code'] . " cannot reduce inventory, not yet packaged.");
-		exit;
-	}
-	if ($row['inventory_reduced']) {
-		syslog(LOG_NOTICE, "code: " . $row['code'] . " cannot reduce inventory, already reduced.");
-		exit;
-	}
-
-	syslog(LOG_NOTICE, "Start reducing inventory from ". $row['code'].' '.$row['name']);
-
-	$fermentables = json_decode($row['json_fermentables'], true);
-	for ($i = 0; $i < count($fermentables); $i++) {
-
-		$sql2  = "UPDATE inventory_fermentables SET inventory = inventory - " . $fermentables[$i]['f_amount'];
-		$sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_name']);
-	        $sql2 .= "' AND origin='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_origin']);
-		$sql2 .= "' AND supplier='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_supplier']);
-	        $sql2 .= "' AND inventory >= " . $fermentables[$i]['f_amount'];
-		$sql2 .= " LIMIT 1;";
-//		syslog(LOG_NOTICE, $sql2);
-		$result2 = mysqli_query($connect, $sql2);
-		$ar = mysqli_affected_rows($connect);
-		if ($ar == 1) {
-			syslog(LOG_NOTICE, "Reduced fermentable `".$fermentables[$i]['f_name']."' from `".$fermentables[$i]['f_supplier']."' with ".$fermentables[$i]['f_amount']." kg");
-		} else if ($ar == 0) {
-			$sql2  = "UPDATE inventory_fermentables SET inventory = 0";
-			$sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_name']);
-		        $sql2 .= "' AND origin='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_origin']);
-			$sql2 .= "' AND supplier='" . mysqli_real_escape_string($connect, $fermentables[$i]['f_supplier']);
-		        $sql2 .= "' AND inventory < " . $fermentables[$i]['f_amount'];
-			$sql2 .= " LIMIT 1;";
-//			syslog(LOG_NOTICE, $sql2);
-			$result2 = mysqli_query($connect, $sql2);
-			$ar = mysqli_affected_rows($connect);
-			if ($ar == 1) {
-				syslog(LOG_NOTICE, "Reduced fermentable `".$fermentables[$i]['f_name']."' from `".$fermentables[$i]['f_supplier']."' to 0 kg");
-			} else if ($ar == 0) {
-				syslog(LOG_NOTICE, "Reduced fermentable `".$fermentables[$i]['f_name']."' from `".$fermentables[$i]['f_supplier']."' failed");
-			}
-//			syslog(LOG_NOTICE, "affected rows: ".$ar);
-		}
-	}
-
-	$hops = json_decode($row['json_hops'], true);
-	for ($i = 0; $i < count($hops); $i++) {
-
-		$sql2  = "UPDATE inventory_hops SET inventory = inventory - " . $hops[$i]['h_amount'];
-		$sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $hops[$i]['h_name']);
-	        $sql2 .= "' AND origin='" . mysqli_real_escape_string($connect, $hops[$i]['h_origin']);
-		$sql2 .= "' AND form=" . $hops[$i]['h_form'];
-		$sql2 .= " AND inventory >= " . $hops[$i]['h_amount'] . " LIMIT 1;";
-//		syslog(LOG_NOTICE, $sql2);
-		$result2 = mysqli_query($connect, $sql2);
-		$ar = mysqli_affected_rows($connect);
-		if ($ar == 1) {
-			syslog(LOG_NOTICE, "Reduced hop `".$hops[$i]['h_name']."' from `".$hops[$i]['h_origin']."' with ".$hops[$i]['h_amount']." kg");
-		} else if ($ar == 0) {
-			$sql2  = "UPDATE inventory_hops SET inventory = 0";
-			$sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $hops[$i]['h_name']);
-		        $sql2 .= "' AND origin='" . mysqli_real_escape_string($connect, $hops[$i]['h_origin']);
-			$sql2 .= "' AND form=" . $hops[$i]['h_form'];
-			$sql2 .= " AND inventory < " . $hops[$i]['h_amount'] . " LIMIT 1;";
-//			syslog(LOG_NOTICE, $sql2);
-			$result2 = mysqli_query($connect, $sql2);
-			$ar = mysqli_affected_rows($connect);
-			if ($ar == 1) {
-				syslog(LOG_NOTICE, "Reduced hop `".$hops[$i]['h_name']."' from `".$hops[$i]['h_origin']."' to 0 kg");
-			} else if ($ar == 0) {
-				syslog(LOG_NOTICE, "Reduced hop `".$hops[$i]['h_name']."' from `".$hops[$i]['h_origin']."' failed");
-			}
-		}
-	}
-
-	$miscs = json_decode($row['json_miscs'], true);
-	for ($i = 0; $i < count($miscs); $i++) {
-
-		$sql2  = "UPDATE inventory_miscs SET inventory = inventory - " . $miscs[$i]['m_amount'];
-		$sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $miscs[$i]['m_name']);
-	        $sql2 .= "' AND inventory >= " . $miscs[$i]['m_amount'] . " LIMIT 1;";
-//		syslog(LOG_NOTICE, $sql2);
-		$result2 = mysqli_query($connect, $sql2);
-		$ar = mysqli_affected_rows($connect);
-		if ($ar == 1) {
-			syslog(LOG_NOTICE, "Reduced misc `".$miscs[$i]['m_name']."' with ".$miscs[$i]['m_amount']);
-		} else if ($ar == 0) {
-			$sql2  = "UPDATE inventory_miscs SET inventory = 0";
-			$sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $miscs[$i]['m_name']);
-		        $sql2 .= "' AND inventory < " . $miscs[$i]['m_amount'] . " LIMIT 1;";
-//			syslog(LOG_NOTICE, $sql2);
-			$result2 = mysqli_query($connect, $sql2);
-			$ar = mysqli_affected_rows($connect);
-			if ($ar == 1) {
-				syslog(LOG_NOTICE, "Reduced misc `".$miscs[$i]['m_name']."' to 0");
-			} else if ($ar == 0) {
-				syslog(LOG_NOTICE, "Reduced misc `".$miscs[$i]['m_name']."' failed");
-			}
-		}
-	}
-
-	$yeasts = json_decode($row['json_yeasts'], true);
-	for ($i = 0; $i < count($yeasts); $i++) {
-
-		$sql2  = "UPDATE inventory_yeasts SET inventory = inventory - " . $yeasts[$i]['y_amount'];
-		$sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_name']);
-	        $sql2 .= "' AND laboratory='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_laboratory']);
-		$sql2 .= "' AND product_id='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_product_id']);
-	        $sql2 .= "' AND form=" . $yeasts[$i]['y_form'];
-		$sql2 .= " AND inventory >= " . $yeasts[$i]['y_amount'] . " LIMIT 1;";
-//		syslog(LOG_NOTICE, $sql2);
-		$result2 = mysqli_query($connect, $sql2);
-		$ar = mysqli_affected_rows($connect);
-		if ($ar == 1) {
-			syslog(LOG_NOTICE, "Reduced yeast `".$yeasts[$i]['y_product_id'].' '.$yeasts[$i]['y_name']."' from `".$yeasts[$i]['y_laboratory']."' with ".$yeasts[$i]['y_amount']);
-		} else if ($ar == 0) {
-			$sql2  = "UPDATE inventory_yeasts SET inventory = 0";
-			$sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_name']);
-		        $sql2 .= "' AND laboratory='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_laboratory']);
-			$sql2 .= "' AND product_id='" . mysqli_real_escape_string($connect, $yeasts[$i]['y_product_id']);
-		        $sql2 .= "' AND form=" . $yeasts[$i]['y_form'];
-			$sql2 .= " AND inventory < " . $yeasts[$i]['y_amount'] . " LIMIT 1;";
-//			syslog(LOG_NOTICE, $sql2);
-			$result2 = mysqli_query($connect, $sql2);
-			$ar = mysqli_affected_rows($connect);
-			if ($ar == 1) {
-				syslog(LOG_NOTICE, "Reduced yeast `".$yeasts[$i]['y_product_id'].' '.$yeasts[$i]['y_name']."' from `".$yeasts[$i]['y_laboratory']."' to 0");
-			} else if ($ar == 0) {
-				syslog(LOG_NOTICE, "Reduced yeast `".$yeasts[$i]['y_product_id'].' '.$yeasts[$i]['y_name']."' from `".$yeasts[$i]['y_laboratory']."' failed");
-			}
-		}
-	}
-
-	if ($row['w1_name'] != '') {
-		$sql2  = "UPDATE inventory_waters SET inventory = inventory - ".$row['w1_amount'];
-		$sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $row['w1_name']);
-		$sql2 .= "' AND unlimited_stock=0 AND inventory >= ".$row['w1_amount']." LIMIT 1;";
-//		syslog(LOG_NOTICE, $sql2);
-		$result2 = mysqli_query($connect, $sql2);
-		$ar = mysqli_affected_rows($connect);
-		if ($ar == 1) {
-			syslog(LOG_NOTICE, "Reduced water `".$row['w1_name']."' with ".$row['w1_amount']." liter");
-		} else if ($ar == 0) {
-			$sql2  = "UPDATE inventory_waters SET inventory = 0";
-			$sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $row['w1_name']);
-			$sql2 .= "' AND unlimited_stock=0 AND inventory < ".$row['w1_amount']." LIMIT 1;";
-//			syslog(LOG_NOTICE, $sql2);
-			$result2 = mysqli_query($connect, $sql2);
-			$ar = mysqli_affected_rows($connect);
-			if ($ar == 1) {
-				syslog(LOG_NOTICE, "Reduced water `".$row['w1_name']."' to 0 liters");
-			} else {
-				syslog(LOG_NOTICE, "Reduced water `".$row['w1_name']."' failed or tapwater");
-			}
-		}
-	}
-	if ($row['w2_name'] != '') {
-		$sql2  = "UPDATE inventory_waters SET inventory = inventory - ".$row['w2_amount'];
-		$sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $row['w2_name']);
-		$sql2 .= "' AND unlimited_stock=0 AND inventory >= ".$row['w2_amount']." LIMIT 1;";
-//		syslog(LOG_NOTICE, $sql2);
-		$result2 = mysqli_query($connect, $sql2);
-		$ar = mysqli_affected_rows($connect);
-		if ($ar == 1) {
-			syslog(LOG_NOTICE, "Reduced water `".$row['w2_name']."' with ".$row['w2_amount']." liter");
-		} else if ($ar == 0) {
-			$sql2  = "UPDATE inventory_waters SET inventory = 0";
-			$sql2 .= " WHERE name='" . mysqli_real_escape_string($connect, $row['w2_name']);
-			$sql2 .= "' AND unlimited_stock=0 AND inventory < ".$row['w2_amount']." LIMIT 1;";
-//			syslog(LOG_NOTICE, $sql2);
-			$result2 = mysqli_query($connect, $sql2);
-			$ar = mysqli_affected_rows($connect);
-			if ($ar == 1) {
-				syslog(LOG_NOTICE, "Reduced water `".$row['w2_name']."' to 0 liters");
-			}
-		}
-	}
-	
-	syslog(LOG_NOTICE, "Finished reducing inventory from ". $row['code'].' '.$row['name']);
-	mysqli_free_result($result1);
-
-	$sql1 = "UPDATE products SET inventory_reduced=1 WHERE uuid = '" . $_POST['uuid'] . "';";
-	//syslog(LOG_NOTICE, $sql1);
-	$result1 = mysqli_query($connect, $sql1);
-	$ar = mysqli_affected_rows($connect);
-}
-
-exit(0);
--- a/www/rec_edit.php	Sun May 12 19:24:34 2019 +0200
+++ b/www/rec_edit.php	Sun May 12 19:41:18 2019 +0200
@@ -94,11 +94,10 @@
         </tr>
        </table>
        <div style="float: right; margin-top: 30px; margin-bottom: 10px;">
-	<input style="margin-right: 100px;" type="button" id="Print"  value="Print" />
         <input style="margin-right: 100px;" type="button" id="Export" value="Export" />
         <input style="margin-right: 100px;" type="button" id="Delete" value="Delete" />
         <input style="margin-right: 100px;" type="button" id="Save"   value="Save" />
-        <input style="margin-right: 230px;" type="button" id="Cancel" value="Cancel" />
+        <input style="margin-right: 320px;" type="button" id="Cancel" value="Cancel" />
        </div>
       </div>
      </div> <!-- tab algemeen -->
--- a/www/rec_export.php	Sun May 12 19:24:34 2019 +0200
+++ b/www/rec_export.php	Sun May 12 19:41:18 2019 +0200
@@ -24,6 +24,14 @@
        <td align="right" style="vertical-align: top;">Copieer als brouw product:</td>
        <td align="left"><div style='margin-left: 10px;' id='jqxRadioButton3'></div></td>
       </tr>
+      <tr>
+       <td align="right" style="vertical-align: top;">Print dit recept:</td>
+       <td align="left"><div style='margin-left: 10px;' id='jqxRadioButton4'></div></td>
+      </tr>
+      <tr>
+       <td align="right" style="vertical-align: top;">Export voor forum:</td>
+       <td align="left"><div style='margin-left: 10px;' id='jqxRadioButton5'></div></td>
+      </tr>
       <tr><td colspan="2">&nbsp;</td></tr>
       <tr>
        <td align="right" style="vertical-align: top;"></td>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/www/rec_forum.php	Sun May 12 19:41:18 2019 +0200
@@ -0,0 +1,237 @@
+<?php
+require_once($_SERVER['DOCUMENT_ROOT'].'/includes/global.inc.php');
+require_once($_SERVER['DOCUMENT_ROOT'].'/includes/formulas.php');
+
+
+$link = mysqli_connect(DBASE_HOST,DBASE_USER,DBASE_PASS,DBASE_NAME);
+if (! $link) {
+	die('Connect Error (' . mysqli_connect_errno() . ') ' . mysqli_connect_error());
+}
+if (! mysqli_set_charset($link, "utf8" )) {
+	echo "error";
+	return 1;
+}
+
+setlocale ( LC_ALL, 'nl_NL.UTF-8');
+$record = $_GET["record"];
+$result = mysqli_query($link, "SELECT * FROM recipes WHERE record='".$record."'");
+$row = mysqli_fetch_array($result);
+
+$preboil_sg = 0;
+date_default_timezone_set('Europe/Amsterdam');
+$prdate = date(DATE_RFC2822);
+
+?>
+<!DOCTYPE html>
+<html lang=nl-NL>
+ <head>
+  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+  <title>BMS v<?php echo $my_version;?> - Export recipe</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1 maximum-scale=1 minimum-scale=1" />
+ </head>
+ <body>
+  <pre>
+<?php
+
+$recipetype  = array( 'Extract', 'Deelmaisch', 'Mout' );
+$colormethod = array( 'Morey', 'Mosher', 'Daniels' );
+$ibumethod   = array( 'Tinseth', 'Rager', 'Daniels' );
+$added       = array( 'Maischen', 'Koken', 'Vergisten', 'Nagisten/lageren', 'Bottelen' );
+$hopform     = array( 'Pellets', 'Plugs', 'Bellen', 'Hop nat' );
+$hopuse      = array( 'Maischen', 'First wort', 'Koken', 'Vlamuit', 'Whirlpool', 'Koudhop' );
+$misctype    = array( 'Specerij', 'Kruid', 'Smaakstof', 'Klaringsmiddel', 'Brouwzout', 'Gistvoeding', 'Anders' );
+$miscuse     = array( 'Starter', 'Maischen', 'Koken', 'Hoofdvergisting', 'Nagisting/lagering', 'Bottelen' );
+$yeastform   = array( 'Vloeibaar', 'Droog', 'Schuine buis', 'Slurry', 'Ingevroren', 'Depot' );
+$yeastuse    = array( 'Hoofdgisting', 'Nagisting', 'Lagering', 'Bottelen' );
+$steptype    = array( 'Infusie', 'Directe verwarming', 'Decoctie' );
+$acidtype    = array( 'Melkzuur', 'Zoutzuur', 'Fosforzuur', 'Zwavelzuur' );
+
+
+function itemline($item, $content) {
+
+	echo $item . " ";
+	for ($i = strlen($item); $i < 40; $i++)
+		echo ".";	
+	echo ": " . $content . PHP_EOL;
+}
+
+
+function formatstr($strin, $len) {
+
+	$strout = null;
+	$cnt = 0;
+
+        for ($i = 0; $i < strlen($strin); $i++) {
+                $ord = ord($strin[$i]);
+		if ($ord != 195)
+			$cnt++;
+
+                switch ($strin[$i]) {
+                        case '<':
+                                        $strout .= '&lt;';
+                                        break;
+                        case '>':
+                                        $strout .= '&gt;';
+                                        break;
+                        case '&':
+                                        $strout .= '&amp;';
+                                        break;
+                        case '"':
+                                        $strout .= '&quot;';
+                                        break;
+                        default:
+                                        $strout .= $strin[$i];
+        	}
+
+		if ($cnt >= $len)
+			break;
+        }
+
+	for ($i = $cnt; $i < $len; $i++)
+		$strout .= " ";
+        return $strout;
+}
+
+
+echo "BMS v" . $my_version . " - Datum export: " . $prdate . PHP_EOL;
+echo "----------------------------------------------------------" . PHP_EOL;
+itemline("Bier naam", $row['name']);
+itemline("Bier stijl", $row['st_name']);
+itemline("Recept type", $recipetype[$row['type']]);
+itemline("Batch grootte", $row['batch_size'].' liter');
+itemline("Kooktijd", $row['boil_time'].' minuten');
+itemline("Brouwzaal rendement", $row['efficiency'].' %');
+itemline("Geschatte begin densiteit", sprintf("%.3f",$row['est_og']).' SG/ '.sprintf("%.1f", sg_to_plato($row['est_og'])).'&deg;P');
+itemline("Geschatte eind densiteit", sprintf("%.3f",$row['est_fg']).' SG/ '.sprintf("%.1f", sg_to_plato($row['est_fg'])).'&deg;P');
+itemline("Geschat alcohol", sprintf("%.1f",$row['est_abv']).'% vol');
+itemline("Kleur (" . $colormethod[$row['color_method']] . ")", $row['est_color'] . ' EBC');
+itemline("Bitterheid (" . $ibumethod[$row['ibu_method']] . ")", $row['est_ibu'] . ' IBU');
+echo PHP_EOL . PHP_EOL;
+
+$sugarsm = 0;
+$grainabsorbtion = 0;
+$mashwater = 0;
+
+$arr = json_decode($row['json_fermentables'], true);
+echo "Mout, granen en suikers                  EBC Gewicht kg     % Gebruik tijdens" . PHP_EOL;
+echo "---------------------------------------- --- ---------- ----- ----------------" . PHP_EOL;
+foreach($arr as $item) {
+	$name = formatstr($item['f_name'] . " (" . $item['f_supplier'] . ")", 40);
+	$amount   = floatval($item['f_amount']);
+	$d = $amount * (floatval($item['f_yield']) / 100) * (1 - floatval($item['f_moisture']) / 100);
+        if ($item['f_added']  == 0) {
+        	$d = floatval($row['efficiency']) / 100 * $d;
+                $sugarsm += $d;
+		$grainabsorbtion += $item['f_amount'];
+        }
+	echo sprintf("%s %3.0f %10.3f %5.1f ", $name, floatval($item['f_color']), $amount, floatval($item['f_percentage']));
+	echo $added[$item['f_added']] . PHP_EOL;
+}
+$preboil_sg = estimate_sg($sugarsm, floatval($row['boil_size']));
+echo PHP_EOL . PHP_EOL;
+
+
+echo "Hop                                      Vorm      &alpha; %   IBU   Gram Toevoegen moment" . PHP_EOL;
+echo "---------------------------------------- ------- ----- ----- ------ --------------------" . PHP_EOL;
+$arr = json_decode($row['json_hops'], true);
+foreach($arr as $item) {
+	$name   = formatstr($item['h_name'] . " (" . $item['h_origin'] . ")", 40);
+	$amount = floatval($item['h_amount']) * 1000;
+	$time   = floatval($item['h_time']);
+        $alpha  = floatval($item['h_alpha']);
+	$ibu    = calc_IBU($item['h_useat'], $item['h_form'], $preboil_sg, floatval($row['batch_size']), $amount, $time, $alpha, $row['ibu_method']);
+	$moment = $hopuse[$item['h_useat']];
+        if ($item['h_useat'] == 2) {    // Boil
+        	$moment = "Kook ".$time." minuten";
+        }
+
+	echo sprintf("%s %-7s %5.1f %5.1f %6.1f %-20s", $name, $hopform[$item['h_form']], $alpha, $ibu, $amount, $moment);
+	echo PHP_EOL;
+}
+echo PHP_EOL . PHP_EOL;
+
+echo "Specerij, kruid, brouwzout etc Type grondstof  Gebruik tijdens          Hoeveel" . PHP_EOL;
+echo "------------------------------ --------------- -------------------- -----------" . PHP_EOL;
+$arr = json_decode($row['json_miscs'], true);
+foreach($arr as $item) {
+	$amount  = floatval($item['m_amount']) * 1000;
+	$aiw     = $item['m_amount_is_weight'];
+	$use_use = $item['m_use_use'];
+	$time    = floatval($item['m_time']);
+
+	if ($use_use == 2)      // Boil
+        	$gebruik = sprintf("%s %d min",$miscuse[$use_use],$time);
+        else
+        	$gebruik = $miscuse[$use_use];
+	$hoeveel = sprintf("%.2f %s",$amount,$aiw ? "gr":"ml");
+
+	echo sprintf("%s %-15s %-20s %11s", formatstr($item['m_name'], 30), $misctype[$item['m_type']], $gebruik, $hoeveel) . PHP_EOL;
+}
+echo PHP_EOL . PHP_EOL;
+
+echo "Gistlab en code      Omschrijving                   Gebruik      Vorm            Hoeveel" . PHP_EOL;
+echo "-------------------- ------------------------------ ------------ ------------ ----------" . PHP_EOL;
+$arr = json_decode($row['json_yeasts'], true);
+foreach($arr as $item) {
+	$name    = formatstr($item['y_name'], 30);
+	$product = formatstr($item['y_laboratory']." ".$item['y_product_id'], 20);
+
+	if ($item['y_form'] == 0)       // Liquid
+                $amount = sprintf("%.0f",floatval($item['y_amount']))." pak";
+        else if ($item['y_form'] == 1)  // Dry
+                $amount = sprintf("%.1f",floatval($item['y_amount'])*1000)." gr";
+        else
+                $amount = sprintf("%.0f",floatval($item['y_amount'])*1000)." ml";
+
+	echo sprintf("%s %s %-12s %-12s %10s", $product, $name, $yeastuse[$item['y_use']], $yeastform[$item['y_form']], $amount) . PHP_EOL;
+}
+if ($row['starter_enable'] && $row['prop1_volume']) {
+	$sv = 0;
+	for ($i = 1; $i < 5; $i++) {
+		$pv = "prop".$i."_volume";
+		if (floatval($row[$pv]) && (floatval($row[$pv]) > $sv)) {
+			$sv = floatval($row[$pv]);
+		}
+	}
+	echo PHP_EOL . "Maak een giststarter van " . sprintf("%.1f", $sv) . " liter." . PHP_EOL;
+}
+echo PHP_EOL . PHP_EOL;
+
+if ($row['w1_name'])
+	$mashwater += floatval($row['w1_amount']);
+if ($row['w2_name'])
+	$mashwater += floatval($row['w2_amount']);
+
+echo "Maisch stap          stap type              stap temp    rusten  opwarmen" . PHP_EOL;
+echo "-------------------- ---------------------- --------- --------- ---------" . PHP_EOL;
+$arr = json_decode($row['json_mashs'], true);
+foreach($arr as $item) {
+	
+	if ($item['step_type'] == 1) {
+		$stype = formatstr($steptype[$item['step_type']], 22);
+	} else {
+		$stype = formatstr($steptype[$item['step_type']].' '.sprintf("%.1f", $item['step_infuse_amount']).' liter', 22);
+	}
+	echo sprintf("%s %s %s %s %s", 
+		formatstr($item['step_name'], 20), $stype, sprintf("%6.1f &deg;C", $item['step_temp']),
+		sprintf("%4.0f min.", $item['step_time']), sprintf("%4.0f min.", $item['ramp_time'])) . PHP_EOL;
+}
+echo PHP_EOL;
+if ($row['w1_name'] && $row['w2_name']) {
+	itemline("Maischwater 1", sprintf("%.1f",floatval($row['w1_amount'])).' liter water '.$row['w1_name']);
+	itemline("Maischwater 2", sprintf("%.1f",floatval($row['w2_amount'])).' liter water '.$row['w2_name']);
+} else {
+	itemline("Maischwater", sprintf("%.1f",floatval($row['w1_amount'])).' liter water '.$row['w1_name']);
+}
+itemline("Maischwater aanzuren tot pH", $row['mash_ph'].' pH');
+$spoelw = ($row['boil_size'] - $mashwater + $grainabsorbtion + $row['eq_lauter_deadspace']) * 1.03;     // A small heat correction
+itemline("Spoelwater geschat", sprintf("%.1f", $spoelw)." liter");
+itemline("Spoelwater temperatuur", $row['sparge_temp'].' &deg;C');
+itemline("Spoelwater aanzuren tot pH", sprintf("%.1f",$row['sparge_ph']).' pH');
+echo PHP_EOL . PHP_EOL;
+
+
+?>
+  </pre>
+ </body>
+</html>

mercurial