Shipping Batches & Cost Calculation
Shipping batches
Before submitting to the shipping methods, we will need to group products in a way that their shipping relationships, as well as product information, does not conflict with the rest of the products. The goal is to have each batch to contain products with the same shipping methods, rates, and packages. Each batch will be packed independently and once packed, they will be considered as one product and will be processed by the shipping methods. The batching process is as follows:
Get all the cart items and group them by their shipping methods
If an item is binded to a shipping method, then we will use that as the item’s shipping methods
Otherwise we get all the shipping methods and check if the product is ok with the attributes of the shipping method. All shipping methods that pass this checking will be added as a shipping method of that product
We will then combine products with similar shipping methods into one batch. For example Product 1 is bound to Auspost and Ship By Volume, while Product 2 is not bound to anything. We will merge product 1 and 2 together with Auspost and Ship By Volume as shipping methods since Product 2 has no restriction. If Product 2 however is bound to Auspost and Flat rate, then we will have 1 batch with only Auspost as shipping method since this is what is similar to both products.
For each batches returned by step 1, we further break the batch by the shipping rates of the items
If an item is binded to a shipping rate, we will use that as the item’s shipping rates.
Otherwise we get all the shipping rates and check if the product’s attributes are within the capacity of the rate. All shipping rates that pass this checking will be added as a shipping rate of that product
We will then combine products with similar packages to minimize the batches same as with Step1.a.
For each batches returned by step 2, we further break the batches by their packages.
If an item is binded to a shipping package, we will use that as the items’s shipping package
Otherwise we get all the shipping packages that fit into the product and use that as applicable packages.
We will then combine products with similar packages to minimize the batches same as with Step1.a.
- Unpackable items will be in a separate batch
Shipping Batches Front End Integration
Because we can have multiple shipping methods in a single order, modules/prodcatalogueorder/templates/payment.html should be updated to support this. Here is an example template:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 | < div class = "row" > < section class = "large-12 columns" > < hr > </ section > </ div > < form action="/index.php?<{$return_page}>" name="payment" method="post" class="payment_frm"> < div class = "row" > < section class = "large-6 columns" > < div class = "row" > < section class = "large-12 columns" > <{if $prodcatalogueorder_err}> < div class = "error" > <{$prodcatalogueorder_err|replace:"Delivery Method: Please select a delivery method":"Shipping & Delivery Options: Please select your Shipping & Delivery Option" }> </ div > <{/if}> < h2 >Shipping & Delivery Options</ h2 > <{foreach item='batch' key="batch_number" from=$shipping_method_batches}> < h3 >Batch <{$batch_number+1}></ h3 > <{foreach item='product' from=$batch.items}> <{$product.name}> < br > <{/foreach}> < select name="delivery[<{$batch_number}>]" class="dk" data-delivery-id> < option value = "" >Please select your Shipping & Delivery Option</ option > <{if !empty($batch.methods)}> <{foreach from=$batch.methods item='method'}> < option value="<{$method->getId()}>" <{if $method->getId() eq $smarty.session.delivery[$batch_number] OR $smarty.post.delivery[$batch_number] eq $method->getId()}>selected<{/if}>/> < span ><{$method->getName()|stripslashes}></ span ></ option > <{/foreach}> <{/if}> </ select > < select style = "display:none;" data-shipping-service-code name="shipping_service_code[<{$batch_number}>]" id="selectId" class="shippingservice dk"></ select > < select style = "display:none;" name = "shippingserviceAusPostOption" id = "selectIdOption" class = "shippingserviceoption dk" ></ select > < p class = "out-of-area-msg" ></ p > <{/foreach}> </ section > </ div > < div class = "row" > < section class = "large-12 columns" > < p class = "terms" >* For security purposes, we are unable to email gift certificates.</ p > < p class = "terms" >** There is a 4pm deadline for Express Post.</ p > </ section > </ div > < hr /> < h2 >Payment details</ h2 > < input type = "hidden" name = "action" value = "prodcatalogueorder" > < input type = "hidden" name = "form_name" value = "payment" > < input type = "hidden" name = "form_action" value = "update" > < label >Voucher</ label > < div >< input type = "text" name = "voucher_promo_code" class = "voucher_promo_code" value="<{$smarty.post.voucher_promo_code}>"></ div > < div >< input type = "button" class = "primary-btn apply-voucher" value = "Submit" ></ div > < div style = "clear:both;" ></ div > <{*< input type = "hidden" name = "cas_istest" value = "1" >*}> <{if $smarty.request.test eq '1'}>< input type = "hidden" name = "test" value = "1" /><{/if}> <{if $smarty.request.guest eq '1'}> < input type = "hidden" name = "guest" value = "1" /> <{/if}> < input type = "hidden" name = "subject" value="Order Confirmation for <{$smarty.session.purchaser_information.billing_firstname}> <{$smarty.session.purchaser_information.billing_lastname}>"> <!--<input type="hidden" name="require" value="terms" />--> <{foreach name=main from=$config_options key=curkey item=curoption}> < div class = "form-row" > < label class="normal-text input-radio <{if $smarty.post.payment eq $curoption->getID()}>checked<{elseif $smarty.post.payment eq '' and $smarty.session.payment eq $curoption->getID()}>checked<{/if}>"> < input type = "radio" name = "payment" value="<{$curoption->getID()}>" <{if $smarty.post.payment eq $curoption->getID()}>checked="checked" <{elseif $smarty.post.payment eq '' and $smarty.session.payment eq $curoption->getID()}>checked="checked" <{/if}> onclick="<{if $curoption->getID() eq '21'}>$('.voucher_promo_code').val('complementary_code');$('.free').show();$('.not-free').hide();<{else}>$('.voucher_promo_code').val('');$('.free').hide();$('.not-free').show();<{/if}>" /> <{$curoption->getName()|stripslashes}> </ label > <{if $curoption->getID() eq '17'}> < div class = "payment-more-details" > < div class = "form-row" > < label >Cardholder's name< span class = "require-star" >*</ span ></ label > < input type = "text" name = "cc_owner" value = "" placeholder = "Cardholder's name" class = "required" /> </ div > < div class = "form-row" > < label >Credit card number< span class = "require-star" >*</ span ></ label > < input type = "text" name = "cc_number" value = "" placeholder = "Credit card number" class = "required" /> </ div > < div class = "form-row" > < label class = "show-tooltip" >Security code< span class = "require-star" >*</ span >< span class = "tooltip" >< p >You can find 3 or 4 digit code on your card</ p ></ span ></ label > < input type = "text" name = "cc_card_validation" value = "" placeholder = "Security code" class = "required" /> </ div > < div class = "form-row" > < label >Expiry date< span class = "require-star" >*</ span ></ label > < div class = "row" > < div class = "large-6 columns" > < select name = "cc_expires_month" class = "required" > < option value = "01" >01</ option > < option value = "02" >02</ option > < option value = "03" >03</ option > < option value = "04" >04</ option > < option value = "05" >05</ option > < option value = "06" >06</ option > < option value = "07" >07</ option > < option value = "08" >08</ option > < option value = "09" >09</ option > < option value = "10" >10</ option > < option value = "11" >11</ option > < option value = "12" >12</ option > </ select > </ div > < div class = "large-6 columns" > <{assign var=startyear value=$smarty.now|date_format:"%Y"}> <{assign var=endyear value=$startyear+6}> < select name = "cc_expires_year" class = "required" > <{section name="year_list" loop=$endyear start=$startyear step=1}> < option value="<{$smarty.section.year_list.index-2000}>"><{$smarty.section.year_list.index}></ option > <{/section}> </ select > </ div > </ div > </ div > < hr /> </ div > <{/if}> <{if $curoption->getID() eq '21' && $user->isMemberOfGroupID('5')}> < div class = "payment-more-details hide" > < div class = "form-row" > < textarea type = "text" name = "order_comments" class = "required" ><{$smarty.post.order_comments}></ textarea > < label >Complementary Password</ label > < input type = "password" name = "complementary_password" /> </ div > < hr /> </ div > <{/if}> </ div > <{/foreach}> </ section > < section class = "large-6 columns" > <{show_block module='prodcataloguecart' block='show_cart' template_name="block_cart_payment"}> </ section > </ div > < div class = "row" > < section class = "large-12 columns" > < hr /> < label class="normal-text input-checkbox <{if $smarty.post.agree eq 'on'}>checked<{/if}>"> < input type = "checkbox" name = "agree" class = "agree" <{if $smarty.post.agree eq 'on'}>checked="checked"<{/if}> /> I have read the < a href = "/page/terms-conditions" target = "_blank" >terms and conditions</ a > </ label > </ section > </ div > < div class = "row" > < section class = "large-12 columns make-payment-wrapper" > < hr /> < a class = "primary-btn left" href="/index.php? action = prodcatalogueorder <{if is_object($user) and $user->getID() ne '2'}><{else}>&guest=1<{/if}>">Back</ a > < a href = "#" class = "primary-btn right make-payment" >Make Payment</ a > </ section > </ div > </ form > < script > var handling_total = 0; // $('.apply-voucher').on('click', function(){ updateCartData(); }); $('[name="delivery"]').on('change', function(){ updateCartData(); }); //$('[name="payment"]').on('change', function(){ updateCartData(); }); function updateCartData() { var promocode = $('[name="voucher_promo_code"]').val(), delivery = $('[name="delivery"]').val(); payment = $('[name="payment"]:checked').val(); if (typeof delivery == 'undefined') { delivery = ''; } if (typeof payment == 'undefined') { payment = ''; } var url = '?action=prodcatalogueorder&form_name=preview&type=json'; url+= '&voucher_promo_code='+promocode; url+= '&delivery='+delivery; url+= '&payment='+payment; $('.loading').show(); $('.loadingbtn').hide(); $.ajax({ dataType: "json", url: url, success: function(data){ if (isNaN(data.handling) || data.handling == null) { data.handling = 0; } $('#subtotal').html('$'+parseFloat(data.subtotal).toFixed(2)); $('#discount').html('$'+parseFloat(data.discount).toFixed(2)); $('#delivery').html('$'+parseFloat(data.handling).toFixed(2)); //$('#handling').html('$'+parseFloat(data.handling).toFixed(2)); $('#tax').html('$'+parseFloat(data.tax).toFixed(2)); $('.total').html('$'+parseFloat(data.total).toFixed(2)); $('#surcharge').html('$'+parseFloat(data.surcharge).toFixed(2)); $('.loading').hide(); $('.loadingbtn').show(); /*exposing variable data globally-need to access inside foreach loop*/ window.prodcatalogueorderJsonResponse = data; if(data.shipping_services != null){ $('.out-of-area-msg').empty(); if($('[name="shipping_service_code"]').length ==0){ $('[name="delivery"]').after('< select name = "shipping_service_code" ></ select >'); } else{ $('[name="shipping_service_code"]').empty(); } $.each(data.shipping_services,function(){ var option = '< option value = "'+this.code+'" data-handling = "'+this.price+'" data-orderoveralltotal = "'+prodcatalogueorderJsonResponse.total+'" >'+ this.name + ' - $'+ (this.speed!=null?this.speed + ' - ':'') + this.price +'</ option >'; $('[name="shipping_service_code"]').append(option); /*trigger update delivery price*/ updateDeliveryPriceForFedex($('[name="shipping_service_code"]')); /*event handler*/ $('[name="shipping_service_code"]').on('change',function(){updateDeliveryPriceForFedex(this);}); $('[name="shipping_service_code"]').show(); }); } else{ //not allowed to checkout $('[name="shipping_service_code"]').remove(); } if(data.out_of_area_message!= null){ $('.out-of-area-msg').html(data.out_of_area_message); $('.out-of-area-msg').addClass('error'); } else{ $('.out-of-area-msg').empty(); $('.out-of-area-msg').removeClass('error'); } } }); } function updateDeliveryPriceForFedex(batch){ var total_delivery_price = 0, overall_total = 0; $('[name^="shipping_service_code"]').each(function(){ var deliveryPrice = $(this).find('option:selected').data('handling'), total = $(this).find('option:selected').data('orderoveralltotal'); if (deliveryPrice) { total_delivery_price += parseFloat(deliveryPrice); } if (total) { overall_total = parseFloat(total); } }); if (typeof handling_total !== 'undefined') { total_delivery_price += parseFloat(handling_total); } $('#delivery').html('$' + total_delivery_price.toFixed(2)); //update overall total var computedTotal = overall_total + total_delivery_price; $('.total').html('$' + parseFloat(computedTotal).toFixed(2)); previous_batch_index = 'batch'+batch; } $(function(){ updateCartData(); }); function updateCartData2($calling_dropdown) { var promocode = $('[name="voucher_promo_code"]').val(), delivery = $('[data-delivery-id]').serialize(), selected_shipping_services = $('[data-shipping-service-code]').serialize(), payment = $('[name="payment"]:checked').val(); if (typeof delivery == 'undefined') { delivery = ''; } if (typeof selected_shipping_services == 'undefined') { selected_shipping_services = ''; } if (typeof payment == 'undefined') { payment = ''; } var params = { action: 'prodcatalogueorder', form_name: 'preview', type: 'json', voucher_promo_code: promocode, payment: payment }; var url = '?' + $.param(params) + '&' + delivery + '&' + selected_shipping_services; $('.loading').show(); $('.loadingbtn').hide(); $.ajax({ dataType: "json", url: url, success: function(data){ if (isNaN(data.handling) || data.handling == null) { data.handling = 0; } var deliveryDiscountTxt = ''; handling_total = parseFloat(data.handling).toFixed(2); var handling = '$'+handling_total; if (data.discount_shipping) { handling_total = parseFloat(parseFloat(data.handling) - parseFloat(data.discount_shipping)).toFixed(2); handling = '$' + handling_total; handling += ' < small style = "color:red; text-decoration: line-through" >$' + parseFloat(data.handling).toFixed(2) + '</ small >'; } $('#subtotal').html('$'+parseFloat(data.subtotal).toFixed(2)); $('#discount').html('$'+parseFloat(data.discount).toFixed(2)); $('#delivery').html(handling); //$('#handling').html('$'+parseFloat(data.handling).toFixed(2)); $('#tax').html('$'+parseFloat(data.tax).toFixed(2)); $('.total').html('$'+parseFloat(data.total).toFixed(2)); $('#surcharge').html('$'+parseFloat(data.surcharge).toFixed(2)); $('.loading').hide(); $('.loadingbtn').show(); /*exposing variable data globally-need to access inside foreach loop*/ window.prodcatalogueorderJsonResponse = data; for (var batch = 0; batch < data.shipping_batches.length ; batch++) { var current_data = data.shipping_batches[batch]; var $parent = $('[ name = "delivery['+batch+']" ]'), $servicesSelect = $('[ name = "shipping_service_code['+batch+']" ]'); var $outOfAreaMessage = $servicesSelect.next('.out-of-area-msg'); if (current_data.shipping_services){ var selected_option = '' ; $outOfAreaMessage.empty(); if($servicesSelect.length == 0){ $parent.after('<select name = "shipping_service_code['+batch+']" ></ select >'); $servicesSelect = $('[name="shipping_service_code['+batch+']"]'); } else{ selected_option = $servicesSelect.val(); $servicesSelect.empty(); } $.each(current_data.shipping_services, function(){ // overwrite the shipping option price with voucher promo price if any if (typeof this.voucher_promo_price != 'undefined' && this.voucher_promo_price !== false) { this.price = parseFloat(this.voucher_promo_price).toFixed(2); } var selected = current_data.shipping_service_code == this.code ? 'selected="selected"' : ''; var option = '< option '+selected+' value = "'+this.code+'" data-handling = "'+this.price+'" data-orderoveralltotal = "'+prodcatalogueorderJsonResponse.total+'" >'+ this.name + ' - $'+ (this.speed!=null?this.speed + ' - ':'') + this.price +'</ option >'; $servicesSelect.append(option); }); /*event handler*/ $servicesSelect.on('change',function(){updateDeliveryPriceForFedex();}); if (selected_option) { $servicesSelect.val(selected_option); } /*trigger update delivery price*/ updateDeliveryPriceForFedex(); $servicesSelect.show(); } else{ //not allowed to checkout $servicesSelect.remove(); } if(current_data.out_of_area_message){ $outOfAreaMessage.html(current_data.out_of_area_message); $outOfAreaMessage.addClass('error'); } else{ $outOfAreaMessage.empty(); $outOfAreaMessage.removeClass('error'); } } } }); } $('[data-delivery-id]').change(function() { updateCartData2($(this)); }); $('.apply-voucher').on('click', function(){ updateCartData2(); }); </ script > |
Order Preview
The order preview returns the calculations across shipping batches via `shipping_batches` parameter. This variable is triggered when 'delivery' field submitted is an array brought about by the shipping batches.
Backwards Compatibility
The original structure of preview data is preserved for backwards compatibility. This is triggered when the submitted 'delivery' field is not array.
Email Notification
The shipping breakdown across shipping batches is available on new_order_lettter.html via the variable <{$shipping_transactions}>