Author Archives: markn

About markn

Mark is the owner and founder of Timesheets MTS Software, an mISV that develops and markets employee timesheet and time clock software. He's also a mechanical engineer, father of four, and a lifelong lover of gadgets.

WooCommerce – Custom Product Fields and WooCommerce Fields Factory

WooCommerce does have ‘attributes’ available for products but like most things in WC these are tailored for users selling clothing and various other consumer products. In my application each product is unique and once sold gone forever. Each product has common characteristics though, such as year of manufacture, metal it is made from, and where it was manufactured. As a matter of course the user of the WooCommerce store wanted to define a set of characteristics that would be entered when a new product was created and then wanted those characteristics displayed in the product listing. A good solution was arrived at through the use of the WooCommerce Fields Factory plugin and modifying the Storefront Single Product->Tabs->description.php template.

The WC Fields Factory plugin allows you to add custom fields to your WooCommerce products. It has the added benefit of allowing you to display those fields on the admin side where they can be edited at the product level by your shop admins. Nifty. In my application I created a group of Admin Fields called “Coin Attributes” and then populated the group with the fields that the customer wanted for their products.

Once that was done and I made sure to check the Hide in Product Page and Hide in Cart & Checkout Page for each field. Now when I add a product I see this in the General tab for each product:

So now we can store the data with each product as needed. The next step is to make sure the data displays as required for products. The key is that we only want the data field to display if there’s a value set. To do that we just need to do a bit of tweaking to the Storefront Single Product->Tabs->description.php template. We do that by making a copy of it into the /woocommerce/single-product/tabs folder of our child theme. In our case we just wanted to display the data in a table. So the PHP code required looked like this:

    //get the custom field data using the get_post_meta function.  All WC Field Factory fields are prefixed with wccaf_ and then the field name
    $coin_type=get_post_meta( $post->ID, "wccaf_coin_type", true );
    $krause_number=get_post_meta( $post->ID, "wccaf_krause_number", true );
    $mint=get_post_meta( $post->ID, "wccaf_mint", true );
    $mintage=get_post_meta( $post->ID, "wccaf_mintage", true );    

//display information only if there's a value set
    if (strlen($coin_type)>0)
        $coin_information.='<tr><td class="coin-information"><strong>Coin Type:</strong></td><td class="coin-information">'.$coin_type.'</td></tr>';
    if (strlen($krause_number)>0)
        $coin_information.='<tr><td class="coin-information"><strong>Krause Number:</strong></td><td class="coin-information">'.$krause_number.'</td></tr>';    
    if (strlen($mint)>0)
        $coin_information.='<tr><td class="coin-information"><strong>Mint:</strong></td><td class="coin-information">'.$mint.'</td></tr>';         
    if (strlen($mintage)>0)
        $coin_information.='<tr><td class="coin-information"><strong>Mintage:</strong></td><td class="coin-information">'.$mintage.'</td></tr>';  

    if (strlen($coin_information)>0)
        echo '<table class="coin-information">'.$coin_information.'</table>';

Of course where exactly you echo out the data in the TABS template is up to you and determined by how your product pages are laid out.

WooCommerce – Display SOLD Instead of Price for Sold Products

If you’re using WooCommerce and selling one-off products you don’t necessarily want to use the default terminology for sold out products. You might want to hide the sold price and remove the WooCommerce “In Stock” messages that are better suited to sites selling t-shirts and so on. Removing the stock availability messages is simple enough using the woocommerce_get_availability filter.

    add_filter('woocommerce_get_availability', 'my_get_availability', 1, 2);

    function my_get_availability($availability, $_product)
        $availability = '';

Couldn’t be any simpler, just set the availability text to an empty string.  Presto chango those messages are gone.

Replacing the price with SOLD when a product is sold is almost as simple, it just requires hooking into a couple more filters.

    add_filter("woocommerce_variable_sale_price_html", "my_remove_prices", 10, 2);
    add_filter("woocommerce_variable_price_html", "my_remove_prices", 10, 2);
    add_filter("woocommerce_get_price_html", "my_remove_prices", 10, 2);
    function my_remove_prices($price, $product)
        if (!$product->is_in_stock())
            $price = "SOLD";
        return $price;

WooCommerce – Add Phone Numbers to Header of Storefront Theme

I happen to like web stores that have contact details in prominent locations so it’s useful to be able to add phone numbers to the header of a site using WooCommerce. In my particular application I’m using the Storefront theme.  Firstly you need a  bit of CSS in your child theme’s style.css file. Your widths might be different and I’ve adjusted the margin of the site-search div to compress things a bit.

.header-contact-details {
    width: 50.7391304348%;
    float: right;
    margin-right: 0;
    clear: none;


Then you can use the storefront_header action to insert some code into the header.  To set the priority of the insertion have a look at the storefront header.php file.  In  my case I wanted the new HTML in between storefront_product_search and storefront_primary_navigation_wrapper so I set the priority to land in between those two.

//add phone numbers to header

    add_action('storefront_header', 'storefront_add_header_phone', 41);

    function storefront_add_header_phone()

        <div class="header-contact-details" style="text-align:right"><p style="color: rgb(64, 48, 74);font-weight:600;font-size:20px;"><i class="fa fa-phone" style="color:rgb(155, 126, 230)"></i> 555 555 555 (AUS)  <i class="fa fa-phone" style="color:rgb(155, 126, 230)"></i> +61 555 555 555 (INTL)</p></div>

The code is pretty clear, I make use of the font awesome font set to have some pretty phone icons and colour them to match the rest of the site style. Of course there’s a little bit of CSS inline here that could be moved into the style.css file but for now it works and I am happy with that. Here’s the what the end result looks like:

WooCommerce – Receive Copies of Customer Emails

If a user is new to WooCommerce it can be a little confusing what emails a customer is receiving and exactly what events are triggering the delivery of emails. To help alleviate the problem it can be useful to deliver copies of all customer emails (see the WooCommerce Email Notifications screen below) to another email address so they can be monitored.

WooCommerce Email Notifications Screen

In this case the woocommerce_email_headers() filter is useful. It allows you to intercept the email header and inject your own custom headers of the same form used by the PHP mail function. In my case I was happy to include a CC email address so I used this code:

//Send copies of customer emails to
add_filter( 'woocommerce_email_headers', 'copy_customer_emails_headers_filter_function', 10, 2);

function copy_customer_emails_headers_filter_function( $headers, $object ) 
    if ($object == 'customer_completed_order' || $object == 'customer_invoice' || $object == 'customer_note' || $object == 'customer_on_hold_order' || $object == 'customer_processing_order' || $object == 'customer_refunded_order') 
        $headers .= "CC: Someone <>" . "\r\n";

    return $headers;

The code itself is fairly self explanatory. It checks the passed object and if it’s one of the customer notification emails then the “CC: Someone <>” . “\r\n” string is appended to the email headers. That email address will now receive a copy of all the emails sent to a customer. Of course the customer will see the CC address in their emails so you could include a BCC address with something like “Bcc: Someone<>\r\n”.

WooCommerce – Default Manage Stock and Sold Invidually Values

I’ve had the somewhat dubious pleasure of launching a new WordPress / WooCommerce site for an established collectables business. The site replaced an aging cart system, that was frankly a steaming pile. Almost every item sold by this business is unique so they wanted to default the values for Manage Stock to true and Sold Individually to true.

The WooCommerce Inventory Screen

It turns out the easiest way to do this is to add some javascript to the admin_enqueue_scripts action that checks the form elements we need checked. Poking about with the Chrome element inspector at the WooCommerce stock handling form told me we needed the _manage_stock and _sold_individually checkboxes ticked and to set a value in the _stock text field. Here’s the code I put in my child theme’s functions.php file.

//set default stock handling values

    add_action('admin_enqueue_scripts', 'wc_default_variation_stock_quantity');

    function wc_default_variation_stock_quantity()
        global $pagenow, $woocommerce;

        $default_stock_quantity = 1;
        $screen = get_current_screen();

        if ( $pagenow == 'post-new.php' && $screen->post_type == 'product')
            <script src="//"></script>
            <script type="text/javascript">
                jQuery(document).ready(function () {
                    if (!jQuery('#_manage_stock').attr('checked')) {
                        jQuery('#_manage_stock').attr('checked', 'checked');
                    if ('0' === jQuery('#_stock').val()) {
                        jQuery('#_stock').val(<?php echo $default_stock_quantity; ?>);
                    if (!jQuery('#_sold_individually').attr('checked')) {
                        jQuery('#_sold_individually').attr('checked', 'checked');


The code itself is fairly simple. The javascript is injected into the document.ready function but only if we’re adding a new post and the post type is WooCommerce’s product post type. The appropriate checkboxes have their checked attribute set to true and the _stock field is set to a value of 1 if it’s currently zero. Simples!

Munich Dunkel

With summer coming to an end I am looking for a darker winter beer to drink that is an easy quaff. Sticking with my recent history of German styles I bought a couple of commercial Dunkels and liked what I tasted. So Dunkel it is. The Dunkel is a dark, transparent lager brewed with darker Munich malts and sometimes some pilsner malt. It is supposed to have a sweeter dark malt profile with a moderate spicy hop aroma. I surfed around various beer recipe sites before settling on a simple recipe that I thought would suit my palate. I adapted the recipe using the trusty BIAB recipe spreadsheet tweaking it for my usual no chill / 30 minute technique.

13 February 2017

Munich Dunkel

12.5L BIAB

Munich 1 Malt 1.4kg (11.29EBC)
Pilsner Malt 1.4kg (4.56EBC)
CaraAmber 0.25kg (9.96EBC)
Carafa 2 0.1kg (35.15EBC)

Pitch onto Fermentis SAFLAGER 34/70 yeast cake from a previous Bohemian Pilsner brew.

15L strike water at 72C with 69C target mash temp. Stir every 20 minutes of mash. The temperature at the start of the mash was 67.6C and at the end of the mash the temperature was 65.2C. At that point I squeezed the bag and sparged it with 4L of cold water. This resulted in 17.5L of pre-boil wort with an SG of 1.036 at 50C. Temperature corrected to 1.045.

30 minute boil

6g Nugget 15.5%AA @ 30 minutes
20g Saaz 4.4%AA @ 10 minutes
1/4 of a Whirlfloc tablet @ 10 minutes.

This hop schedule gave a projected bitterness right at the bottom limit of the style. Because I no chill I’ve perceived that a lot of my beers have been too bitter even when counting flameout and 5 minute additions as 10 minute additions. So when devising this recipe I used those allowances AND aimed for IBU’s at the bottom limit of the style hoping it would end up right on target.

As usual when the boil was done I stuck the whole pot in a my fermentation fridge and allowed it to get the wort down to 12.5C. The next day when it was at the correct temperature I tipped the lot onto the yeast cake of my last brew and aerated it vigorously with my giant brew spoon. Original gravity was finalised at 1.050. There was 14.5L of wort in the fermenter. I’ll be using the usual fast lagering method for this beer before bulk priming and bottling.

Fermentation Log

14-2-17 – Into fridge at 12C
27-2-17 – ramp temp up to 20C over three days
2-3-17 – Cold crash to 3C
4-3-17 – Bottle with 130g of priming sugar to get 18.5 740mL bottles. FG measured at 1.019 which was too high. Perhaps due to not leaving at 20C long enough?


Well I couldn’t wait with this one and opened a bottle just 8 days after bottling and surprisingly it was reasonably carbonated and poured fairly clear. It had a lovely creamy tan head which didn’t hang around too long most likely due to it not being in the bottle long enough. It was super easy to drink and very toasty with just a hint of the dark malt burnt flavor which is not really appropriate for the style. To my palate the bitterness was spot on and it went down just a bit too quickly! I don’t see this brew hanging around too long and can’t wait to try it again to iron out a couple of wrinkles in the process and perhaps tone down the impact of the Carafa 2 specialty malt a little bit.

Munich Dunkel

Bohemian Pilsner with Flameout Hop Addition

This is the third time I’ve brewed this particular Bohemian Pilsner recipe with the only variation being a flameout hop addition of Czech Saaz. Last time I brewed it the hop flavour was a bit grassy and overpowering, so I’ve dropped the addition to 25g down from 35g last time.

29 January 2017

Bohemian Pilsner

12.5L BIAB

3kg Gladfield Pilsner Malt (3.8EBC)
0.26kg Weyermann Vienna Malt (7.5EBC)
Fermentis SAFLAGER 34/70

15L strike water at 72C with 69C target mash temp. Stir every 20 minutes of mash. At the end of the mash the temperature was 67C. At that point I squeezed the bag and rinsed it with 4L of cold water. This resulted in 17.5L of pre-boil wort with an SG of 1.037 at 51C. Temperature corrected to 1.046.

30 minute boil

7g of Warrior 15.5%AA @ 30 minutes
17g Saaz (4.4%AA) @ 15 minutes
15g Saaz (4.4%AA) @ 10 minutes
1/2 whirlfloc @ 10 minutes
25g Saaz (4.4%AA) @ Flameout.

I followed my usual no chill routine to get the wort down to 12.5C. At that point I poured it into my sterilised fermenter and took at gravity reading of 1.053. There was 13.75L of wort in the fermenter which I aerated and then sprinkled the yeast on top. This will be using the Brulosopy Fast Lager method which means leaving it at 12.5C for 5 days before allowing the temperature to ramp up to 20C over 2 days, holding at 20C for a week or so before fining with gelatin and cold crashing.

Fermentation Log

30/1/17 – Set temp of 12.5C.
6/2/17 – +3C each day until 20C
8/2/17->11/2/17 – 20C
11/2/17 – Fine with gelatin
11/2/17->13/2/17 – Cold crash to 1C

13/2/17 bottled after bulk priming w/130g of sugar. Forgot to take final gravity reading before bulk priming, gravity after bulk priming was 1.019.

Australian Pale Ale with Stella / Ella Hops

I haven’t been posting much here recently about my home brewing activities. Actually I haven’t been posting much here about anything. But nevermind. I’m posting now because I just filled up my good old pen and paper notebook and the last half dozen or so brew days are about to be consigned to an archive box so I figured I should really start logging things online again so I can look back at my brew logs more easily.

26 November 2016

Australian Pale ale with Stella / Ella Hops

This was a leftover/bitsa brew. I had a whole packet of Stella / Ella hops (14.6% AA) I wanted to use up. A goal was to keep the grain bill simple, use a single hop, and produce an easy drinking ale.

12.5L BIAB

2.6kg Barret Burston 2 Row Ale
0.14kg Crystal Pale 100
US05 SAFALE yeast

15L strike water at 70C with 68C target mash temp. Stir every 20 minutes of mash. At the end of the mash the temperature was 66C. At that point I squeezed the bag and rinsed it with 4L of cold water. This resulted in 18L of pre-boil wort with an SG of 1.027 at 48C. Temperature corected to 1.035.

45 minute boil

5g Ella @ 30 minutes
5g Ella @ 15 minutes
15g Ella @ 5 minutes
1/2 whirlflow @ 5 minutes
20g Ella @ Flameout

No Chill. As usual for my no chill brews I’ve considered all additions later than 10 minutes as 10 minute additions. At the end of the boil I put the wort, boil pot and all into my fridge and let it cool for 18 hours down to my target fermentation temperature of 18C. I cleaned and sterilised my trusty fermenter, dumped the cooled wort into it, at this point I had 14.5L of wort with a measured gravity of 1.046. I aerated the wort with my big old spoon, sprinkled my yeast, put the lid on and put the fermenter back in the fridge to do it’s think for a couple of weeks. I plan to let it ferment out for a week, fine with gelatin, chill for a few days and then bottle.

Fermentation Log

27/11/16 – Set temp of 18C.
28/11/16 – Fermentation appears to have started.
7/12/16 – Fined with Gelatin
9/12/16 – Cold crash to 3C.
11/11/16 – Bottled, bulk primed with 130g of sugar and total yield was 12.2L of beer.

Opened a bottle 7 days later and it was almost fully carbonated with a creamy pale cream head that last nicely and gave good lacing. Aroma was slightly fruity, and colour was a dark amber, quite a bit darker than I expected. The beer was also quite bitter, perhaps slightly out of balance with the malt character. There’s a couple of reasons why it might have seemed bitter, firstly it might be because it actually was, my no chill method sometimes leads to overly bitter beer especially with lots of late hop additions like this one. Alternatively I might have just perceived it as being more bitter because I’ve been drinking beers with lower IBU’s like Munich Helles (IBU 14.2) and Bohemian Pilsner (25 IBU) while this Ale had a predicted IBU of 33. Anyway, I only noticed the bitterness for the first glass, by the second glass my palate had adjusted and I enjoyed it immensely.

Ella Pale Ale

Ella Pale Ale

If I was to make this beer again I’d probably drop the late hob additions by 5g each.

Managing Bulk URL Exclusions in Adwords

The Google Adwords online interface is horrid. Just abominable. I am sure they all went to iOS UI design school and said, well Apple’s UI is shit but I am sure we can make our stuff shitter. And so they did. Anyway, I manage a very long list of URL exclusions for one of my Display Network campaigns and there’s (apparently) some limit to the number of URL’s you can exclude directly from the campaign placement report. When you exceed the limit (whatever that is) and you try to exclude a URL (using the Edit->Exclude (campaign) option) you get this (fucking) useless error message:


Thanks. A. Lot. You useless pack of dickheads.

Anyway after trying Google’s live support (useless), and their online forums (which only dream of one day being merely useless) I finally worked out that my list of exclusions had gotten too long. So, now I got to dig around in the online Adwords UI looking for a way of having a longer list. There is a way, but oh my goodness, finding it makes Indiana Jones’s quest for the Lost Ark of the Covenant look like a stroll to the local shop for a carton of milk by comparison. Again. Fuck. You. Google. Anyway, you manage the list from within the Campaign->Display Network tab of the Adwords system. Click on the TARGETING button, then look in the PLACEMENTS box (second from the top) and click the stupid little pencil icon to edit the placement list.


Once you’ve done that you’ll see something like that below. Click the LISTS button


Once you’ve done that you’ll be given an option to CREATE AND MANAGE LISTS. Once you’ve clicked on that option you can create a new URL exclusion list which can contain a very large number of exclusions. What is the very large number? I have no clue as the Adwords documentation sucks seven different types of ass. But my list is several thousand URL’s long and it seems to still be working OK.

Anyway, after spending several hours working this out you can probably sense my frustration with the Adwords management system. I get similar levels of frustration when I am forced to use iTunes but I have to say that the Adwords UI has taken it to a whole new level. Congratulations you asshats.

Running a PHP Script on a QNAP TS-431

I needed to setup a persistent cron job on my QNAP TS-431 that would run a PHP script (actually the script that let’s me always know what the IP address of my home router is from anywhere in the world). However, the QNAP has the php executable buried and the directory is not in the $PATH environment variable. It’s not too hard to find though, if you need to run a PHP script just do this:

mnt/ext/opt/apache/bin/php /path/to/some-php-script.php