1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) exit;
4
5 6 7 8 9 10 11 12 13 14
15 class WC_Shipping_Flat_Rate extends WC_Shipping_Method {
16
17 18 19 20 21 22
23 function __construct() {
24 $this->id = 'flat_rate';
25 $this->method_title = __( 'Flat Rate', 'woocommerce' );
26 $this->flat_rate_option = 'woocommerce_flat_rates';
27 $this->method_description = __( 'Flat rates let you define a standard rate per item, or per order.', 'woocommerce' );
28
29 add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
30 add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_flat_rates' ) );
31 add_filter( 'woocommerce_settings_api_sanitized_fields_' . $this->id, array( $this, 'save_default_costs' ) );
32
33 $this->init();
34 }
35
36 37 38 39 40 41
42 function init() {
43
44
45 $this->init_form_fields();
46 $this->init_settings();
47
48
49 $this->title = $this->get_option( 'title' );
50 $this->availability = $this->get_option( 'availability' );
51 $this->countries = $this->get_option( 'countries' );
52 $this->type = $this->get_option( 'type' );
53 $this->tax_status = $this->get_option( 'tax_status' );
54 $this->cost = $this->get_option( 'cost' );
55 $this->cost_per_order = $this->get_option( 'cost_per_order' );
56 $this->fee = $this->get_option( 'fee' );
57 $this->minimum_fee = $this->get_option( 'minimum_fee' );
58 $this->options = (array) explode( "\n", $this->get_option( 'options' ) );
59
60
61 $this->get_flat_rates();
62 }
63
64
65 66 67 68 69 70
71 function init_form_fields() {
72
73 $this->form_fields = array(
74 'enabled' => array(
75 'title' => __( 'Enable/Disable', 'woocommerce' ),
76 'type' => 'checkbox',
77 'label' => __( 'Enable this shipping method', 'woocommerce' ),
78 'default' => 'no',
79 ),
80 'title' => array(
81 'title' => __( 'Method Title', 'woocommerce' ),
82 'type' => 'text',
83 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
84 'default' => __( 'Flat Rate', 'woocommerce' ),
85 'desc_tip' => true
86 ),
87 'availability' => array(
88 'title' => __( 'Availability', 'woocommerce' ),
89 'type' => 'select',
90 'default' => 'all',
91 'class' => 'availability',
92 'options' => array(
93 'all' => __( 'All allowed countries', 'woocommerce' ),
94 'specific' => __( 'Specific Countries', 'woocommerce' ),
95 ),
96 ),
97 'countries' => array(
98 'title' => __( 'Specific Countries', 'woocommerce' ),
99 'type' => 'multiselect',
100 'class' => 'chosen_select',
101 'css' => 'width: 450px;',
102 'default' => '',
103 'options' => WC()->countries->get_shipping_countries(),
104 'custom_attributes' => array(
105 'data-placeholder' => __( 'Select some countries', 'woocommerce' )
106 )
107 ),
108 'tax_status' => array(
109 'title' => __( 'Tax Status', 'woocommerce' ),
110 'type' => 'select',
111 'default' => 'taxable',
112 'options' => array(
113 'taxable' => __( 'Taxable', 'woocommerce' ),
114 'none' => _x( 'None', 'Tax status', 'woocommerce' )
115 ),
116 ),
117 'cost_per_order' => array(
118 'title' => __( 'Cost per order', 'woocommerce' ),
119 'type' => 'price',
120 'placeholder' => wc_format_localized_price( 0 ),
121 'description' => __( 'Enter a cost (excluding tax) per order, e.g. 5.00. Default is 0.', 'woocommerce' ),
122 'default' => '',
123 'desc_tip' => true
124 ),
125 'options' => array(
126 'title' => __( 'Additional Rates', 'woocommerce' ),
127 'type' => 'textarea',
128 'description' => __( 'Optional extra shipping options with additional costs (one per line): Option Name | Additional Cost [+- Percents] | Per Cost Type (order, class, or item) Example: <code>Priority Mail | 6.95 [+ 0.2%] | order</code>.', 'woocommerce' ),
129 'default' => '',
130 'desc_tip' => true,
131 'placeholder' => __( 'Option Name | Additional Cost [+- Percents%] | Per Cost Type (order, class, or item)', 'woocommerce' )
132 ),
133 'additional_costs' => array(
134 'title' => __( 'Additional Costs', 'woocommerce' ),
135 'type' => 'title',
136 'description' => __( 'Additional costs can be added below - these will all be added to the per-order cost above.', 'woocommerce' )
137 ),
138 'type' => array(
139 'title' => __( 'Costs Added...', 'woocommerce' ),
140 'type' => 'select',
141 'default' => 'order',
142 'options' => array(
143 'order' => __( 'Per Order - charge shipping for the entire order as a whole', 'woocommerce' ),
144 'item' => __( 'Per Item - charge shipping for each item individually', 'woocommerce' ),
145 'class' => __( 'Per Class - charge shipping for each shipping class in an order', 'woocommerce' ),
146 ),
147 ),
148 'additional_costs_table' => array(
149 'type' => 'additional_costs_table'
150 ),
151 'minimum_fee' => array(
152 'title' => __( 'Minimum Handling Fee', 'woocommerce' ),
153 'type' => 'price',
154 'placeholder' => wc_format_localized_price( 0 ),
155 'description' => __( 'Enter a minimum fee amount. Fee\'s less than this will be increased. Leave blank to disable.', 'woocommerce' ),
156 'default' => '',
157 'desc_tip' => true
158 ),
159 );
160
161 }
162
163
164 165 166 167 168 169 170
171 function calculate_shipping( $package = array() ) {
172
173 $this->rates = array();
174 $cost_per_order = ( isset( $this->cost_per_order ) && ! empty( $this->cost_per_order ) ) ? $this->cost_per_order : 0;
175
176 if ( $this->type == 'order' ) {
177
178 $shipping_total = $this->order_shipping( $package );
179
180 if ( ! is_null( $shipping_total ) || $cost_per_order > 0 ) {
181 $rate = array(
182 'id' => $this->id,
183 'label' => $this->title,
184 'cost' => $shipping_total + $cost_per_order,
185 );
186 }
187
188 } elseif ( $this->type == 'class' ) {
189
190 $shipping_total = $this->class_shipping( $package );
191
192 if ( ! is_null( $shipping_total ) || $cost_per_order > 0 ) {
193 $rate = array(
194 'id' => $this->id,
195 'label' => $this->title,
196 'cost' => $shipping_total + $cost_per_order,
197 );
198 }
199
200 } elseif ( $this->type == 'item' ) {
201
202 $costs = $this->item_shipping( $package );
203
204 if ( ! is_null( $costs ) || $cost_per_order > 0 ) {
205
206 if ( ! is_array( $costs ) ) {
207 $costs = array();
208 }
209
210 $costs['order'] = $cost_per_order;
211
212 $rate = array(
213 'id' => $this->id,
214 'label' => $this->title,
215 'cost' => $costs,
216 'calc_tax' => 'per_item',
217 );
218
219 }
220 }
221
222 if ( isset( $rate ) ) {
223 $this->add_rate( $rate );
224 }
225
226
227 if ( sizeof( $this->options ) > 0) {
228
229 if ( ! isset( $rate ) ) {
230 $rate = array(
231 'id' => $this->id,
232 'label' => $this->title,
233 'cost' => 0,
234 );
235 }
236
237
238 $total_quantity = 0;
239
240 foreach ( $package['contents'] as $item_id => $values ) {
241 if ( $values['quantity'] > 0 && $values['data']->needs_shipping() ) {
242 $total_quantity += $values['quantity'];
243 }
244 }
245
246
247 foreach ( $this->options as $option ) {
248
249 $this_option = array_map( 'trim', explode( WC_DELIMITER, $option ) );
250
251 if ( sizeof( $this_option ) !== 3 ) continue;
252
253 $extra_rate = $rate;
254
255 $extra_rate['id'] = $this->id . ':' . urldecode( sanitize_title( $this_option[0] ) );
256 $extra_rate['label'] = $this_option[0];
257 $this_cost = $this_option[1];
258 $this_cost_percents = '';
259
260 $pattern =
261 '/' .
262 '(\d+\.?\d*)' .
263 '\s*' .
264 '(\+|-)' .
265 '\s*'.
266 '(\d+\.?\d*)'.
267 '\%/';
268 if ( preg_match( $pattern, $this_cost, $this_cost_matches ) ) {
269 $this_cost_mathop = $this_cost_matches[2];
270 $this_cost_percents = $this_cost_matches[3] / 100;
271 $this_cost = $this_cost_matches[1];
272 unset( $this_cost_matches );
273 }
274
275
276 if ( $this_option[2] == 'yes' ) {
277 $this_type = 'order';
278 } elseif ( $this_option[2] == 'no' ) {
279 $this_type = $this->type;
280 } else {
281 $this_type = $this_option[2];
282 }
283
284 switch ( $this_type ) {
285 case 'class' :
286 $this_cost = $this_cost * sizeof( $this->find_shipping_classes( $package ) );
287
288
289 if ( $this_cost_percents ) {
290 foreach ( $this->find_shipping_classes( $package ) as $shipping_class => $items ){
291 foreach ( $items as $item_id => $values ) {
292 if ($this_cost_mathop == '+') {
293 $this_cost += $this_cost_percents * $values['line_total'];
294 }
295 else {
296 $this_cost -= $this_cost_percents * $values['line_total'];
297 }
298 }
299 }
300 }
301 break;
302 case 'item' :
303 $this_cost = $this_cost * $total_quantity;
304
305
306 if ( $this_cost_percents ) {
307 foreach ( $package['contents'] as $item_id => $values ) {
308 if ($this_cost_mathop == '+') {
309 $this_cost += $this_cost_percents * $values['line_total'];
310 } else {
311 $this_cost -= $this_cost_percents * $values['line_total'];
312 }
313 }
314 }
315 break;
316 case 'order' :
317
318 if ( $this_cost_percents ) {
319 if ($this_cost_mathop == '+') {
320 $this_cost += $this_cost_percents * $package['contents_cost'];
321 } else {
322 $this_cost -= $this_cost_percents * $package['contents_cost'];
323 }
324 }
325 break;
326 }
327
328
329 if ( is_array( $extra_rate['cost'] ) ) $extra_rate['cost']['order'] = $extra_rate['cost']['order'] + $this_cost;
330
331
332 else $extra_rate['cost'] = $extra_rate['cost'] + $this_cost;
333
334 $this->add_rate( $extra_rate );
335 }
336 }
337 }
338
339
340 341 342 343 344 345 346
347 function order_shipping( $package ) {
348 $cost = null;
349 $fee = null;
350
351 if ( sizeof( $this->flat_rates ) > 0 ) {
352
353 $found_shipping_classes = $this->find_shipping_classes( $package );
354
355
356 foreach ( $found_shipping_classes as $shipping_class => $products ) {
357 if ( isset( $this->flat_rates[ $shipping_class ] ) ) {
358 if ( $this->flat_rates[ $shipping_class ]['cost'] > $cost ) {
359 $cost = $this->flat_rates[ $shipping_class ]['cost'];
360 $fee = $this->flat_rates[ $shipping_class ]['fee'];
361 }
362 } else {
363
364 if ( ! empty( $this->cost ) && $this->cost > $cost ) {
365 $cost = $this->cost;
366 $fee = $this->fee;
367 }
368 }
369 }
370
371 }
372
373
374 if ( is_null( $cost ) && $this->cost !== '' ) {
375 $cost = $this->cost;
376 $fee = $this->fee;
377 } elseif ( is_null( $cost ) ) {
378
379 $cost = 0;
380 $fee = 0;
381 }
382
383
384 return $cost + $this->get_fee( $fee, $package['contents_cost'] );
385 }
386
387
388 389 390 391 392 393 394
395 function class_shipping( $package ) {
396 $cost = null;
397 $fee = null;
398 $matched = false;
399
400 if ( sizeof( $this->flat_rates ) > 0 || $this->cost !== '' ) {
401
402
403 $found_shipping_classes = $this->find_shipping_classes( $package );
404
405
406 $found_shipping_classes_values = array();
407
408 foreach ( $found_shipping_classes as $shipping_class => $products ) {
409 if ( ! isset( $found_shipping_classes_values[ $shipping_class ] ) ) {
410 $found_shipping_classes_values[ $shipping_class ] = 0;
411 }
412
413 foreach ( $products as $product ) {
414 $found_shipping_classes_values[ $shipping_class ] += $product['data']->get_price() * $product['quantity'];
415 }
416 }
417
418
419 foreach ( $found_shipping_classes_values as $shipping_class => $class_price ) {
420 if ( isset( $this->flat_rates[ $shipping_class ] ) ) {
421 $cost += $this->flat_rates[ $shipping_class ]['cost'];
422 $fee += $this->get_fee( $this->flat_rates[ $shipping_class ]['fee'], $class_price );
423 $matched = true;
424 } elseif ( $this->cost !== '' ) {
425
426 $cost += $this->cost;
427 $fee += $this->get_fee( $this->fee, $class_price );
428 $matched = true;
429 }
430 }
431 }
432
433
434 if ( $matched ) {
435 return $cost + $fee;
436 } else {
437 return null;
438 }
439 }
440
441
442 443 444 445 446 447 448
449 function item_shipping( $package ) {
450
451 $costs = array();
452
453 $matched = false;
454
455
456 foreach ( $package['contents'] as $item_id => $values ) {
457 $_product = $values['data'];
458
459 if ( $values['quantity'] > 0 && $_product->needs_shipping() ) {
460 $shipping_class = $_product->get_shipping_class();
461
462 $fee = $cost = 0;
463
464 if ( isset( $this->flat_rates[ $shipping_class ] ) ) {
465 $cost = $this->flat_rates[ $shipping_class ]['cost'];
466 $fee = $this->get_fee( $this->flat_rates[ $shipping_class ]['fee'], $_product->get_price() );
467 $matched = true;
468 } elseif ( $this->cost !== '' ) {
469 $cost = $this->cost;
470 $fee = $this->get_fee( $this->fee, $_product->get_price() );
471 $matched = true;
472 }
473
474 $costs[ $item_id ] = ( ( $cost + $fee ) * $values['quantity'] );
475 }
476 }
477
478 if ( $matched ) {
479 return $costs;
480 } else {
481 return null;
482 }
483 }
484
485 486 487 488 489 490 491
492 public function find_shipping_classes( $package ) {
493 $found_shipping_classes = array();
494
495
496 if ( sizeof( $package['contents'] ) > 0 ) {
497 foreach ( $package['contents'] as $item_id => $values ) {
498 if ( $values['data']->needs_shipping() ) {
499 $found_class = $values['data']->get_shipping_class();
500 if ( ! isset( $found_shipping_classes[ $found_class ] ) ) {
501 $found_shipping_classes[ $found_class ] = array();
502 }
503
504 $found_shipping_classes[ $found_class ][ $item_id ] = $values;
505 }
506 }
507 }
508
509 return $found_shipping_classes;
510 }
511
512 513 514 515 516 517 518
519 function validate_additional_costs_table_field( $key ) {
520 return false;
521 }
522
523 524 525 526 527 528
529 function generate_additional_costs_table_html() {
530 ob_start();
531 ?>
532 <tr valign="top">
533 <th scope="row" class="titledesc"><?php _e( 'Costs', 'woocommerce' ); ?>:</th>
534 <td class="forminp" id="<?php echo $this->id; ?>_flat_rates">
535 <table class="shippingrows widefat" cellspacing="0">
536 <thead>
537 <tr>
538 <th class="check-column"><input type="checkbox"></th>
539 <th class="shipping_class"><?php _e( 'Shipping Class', 'woocommerce' ); ?></th>
540 <th><?php _e( 'Cost', 'woocommerce' ); ?> <a class="tips" data-tip="<?php _e( 'Cost, excluding tax.', 'woocommerce' ); ?>">[?]</a></th>
541 <th><?php _e( 'Handling Fee', 'woocommerce' ); ?> <a class="tips" data-tip="<?php _e( 'Fee excluding tax. Enter an amount, e.g. 2.50, or a percentage, e.g. 5%.', 'woocommerce' ); ?>">[?]</a></th>
542 </tr>
543 </thead>
544 <tfoot>
545 <tr>
546 <th colspan="4"><a href="#" class="add button"><?php _e( 'Add Cost', 'woocommerce' ); ?></a> <a href="#" class="remove button"><?php _e( 'Delete selected costs', 'woocommerce' ); ?></a></th>
547 </tr>
548 </tfoot>
549 <tbody class="flat_rates">
550 <tr>
551 <td></td>
552 <td class="flat_rate_class"><?php _e( 'Any class', 'woocommerce' ); ?></td>
553 <td><input type="text" value="<?php echo esc_attr( wc_format_localized_price( $this->cost ) ); ?>" name="default_cost" placeholder="<?php _e( 'N/A', 'woocommerce' ); ?>" size="4" class="wc_input_price" /></td>
554 <td><input type="text" value="<?php echo esc_attr( wc_format_localized_price( $this->fee ) ); ?>" name="default_fee" placeholder="<?php _e( 'N/A', 'woocommerce' ); ?>" size="4" class="wc_input_price" /></td>
555 </tr>
556 <?php
557 $i = -1;
558 if ( $this->flat_rates ) {
559 foreach ( $this->flat_rates as $class => $rate ) {
560 $i++;
561
562 echo '<tr class="flat_rate">
563 <th class="check-column"><input type="checkbox" name="select" /></th>
564 <td class="flat_rate_class">
565 <select name="' . esc_attr( $this->id . '_class[' . $i . ']' ) . '" class="select">';
566
567 if ( WC()->shipping->get_shipping_classes() ) {
568 foreach ( WC()->shipping->get_shipping_classes() as $shipping_class ) {
569 echo '<option value="' . esc_attr( $shipping_class->slug ) . '" '.selected($shipping_class->slug, $class, false).'>'.$shipping_class->name.'</option>';
570 }
571 } else {
572 echo '<option value="">'.__( 'Select a class…', 'woocommerce' ).'</option>';
573 }
574
575 echo '</select>
576 </td>
577 <td><input type="text" value="' . esc_attr( $rate['cost'] ) . '" name="' . esc_attr( $this->id .'_cost[' . $i . ']' ) . '" placeholder="' . wc_format_localized_price( 0 ) . '" size="4" class="wc_input_price" /></td>
578 <td><input type="text" value="' . esc_attr( $rate['fee'] ) . '" name="' . esc_attr( $this->id .'_fee[' . $i . ']' ) . '" placeholder="' . wc_format_localized_price( 0 ) . '" size="4" class="wc_input_price" /></td>
579 </tr>';
580 }
581 }
582 ?>
583 </tbody>
584 </table>
585 <script type="text/javascript">
586 jQuery(function() {
587
588 jQuery('#<?php echo $this->id; ?>_flat_rates').on( 'click', 'a.add', function(){
589
590 var size = jQuery('#<?php echo $this->id; ?>_flat_rates tbody .flat_rate').size();
591
592 jQuery('<tr class="flat_rate">\
593 <th class="check-column"><input type="checkbox" name="select" /></th>\
594 <td class="flat_rate_class">\
595 <select name="<?php echo $this->id; ?>_class[' + size + ']" class="select">\
596 <?php
597 if (WC()->shipping->get_shipping_classes()) :
598 foreach (WC()->shipping->get_shipping_classes() as $class) :
599 echo '<option value="' . esc_attr( $class->slug ) . '">' . esc_js( $class->name ) . '</option>';
600 endforeach;
601 else :
602 echo '<option value="">'.__( 'Select a class…', 'woocommerce' ).'</option>';
603 endif;
604 ?>\
605 </select>\
606 </td>\
607 <td><input type="text" name="<?php echo $this->id; ?>_cost[' + size + ']" placeholder="<?php echo wc_format_localized_price( 0 ); ?>" size="4" class="wc_input_price" /></td>\
608 <td><input type="text" name="<?php echo $this->id; ?>_fee[' + size + ']" placeholder="<?php echo wc_format_localized_price( 0 ); ?>" size="4" class="wc_input_price" /></td>\
609 </tr>').appendTo('#<?php echo $this->id; ?>_flat_rates table tbody');
610
611 return false;
612 });
613
614
615 jQuery('#<?php echo $this->id; ?>_flat_rates').on( 'click', 'a.remove', function(){
616 var answer = confirm("<?php _e( 'Delete the selected rates?', 'woocommerce' ); ?>");
617 if (answer) {
618 jQuery('#<?php echo $this->id; ?>_flat_rates table tbody tr th.check-column input:checked').each(function(i, el){
619 jQuery(el).closest('tr').remove();
620 });
621 }
622 return false;
623 });
624
625 });
626 </script>
627 </td>
628 </tr>
629 <?php
630 return ob_get_clean();
631 }
632
633 634 635 636 637 638
639 function process_flat_rates() {
640
641 $flat_rate_class = array();
642 $flat_rate_cost = array();
643 $flat_rate_fee = array();
644 $flat_rates = array();
645
646 if ( isset( $_POST[ $this->id . '_class'] ) ) $flat_rate_class = array_map( 'wc_clean', $_POST[ $this->id . '_class'] );
647 if ( isset( $_POST[ $this->id . '_cost'] ) ) $flat_rate_cost = array_map( 'stripslashes', $_POST[ $this->id . '_cost'] );
648 if ( isset( $_POST[ $this->id . '_fee'] ) ) $flat_rate_fee = array_map( 'stripslashes', $_POST[ $this->id . '_fee'] );
649
650
651 $values = $flat_rate_class;
652 ksort( $values );
653 $value = end( $values );
654 $key = key( $values );
655
656 for ( $i = 0; $i <= $key; $i++ ) {
657 if ( ! empty( $flat_rate_class[ $i ] ) && isset( $flat_rate_cost[ $i ] ) && isset( $flat_rate_fee[ $i ] ) ) {
658
659 $flat_rate_cost[ $i ] = wc_format_decimal( $flat_rate_cost[$i] );
660
661 if ( ! strstr( $flat_rate_fee[$i], '%' ) ) {
662 $flat_rate_fee[ $i ] = wc_format_decimal( $flat_rate_fee[$i] );
663 } else {
664 $flat_rate_fee[ $i ] = wc_clean( $flat_rate_fee[$i] );
665 }
666
667
668 $flat_rates[ urldecode( sanitize_title( $flat_rate_class[ $i ] ) ) ] = array(
669 'cost' => $flat_rate_cost[ $i ],
670 'fee' => $flat_rate_fee[ $i ],
671 );
672 }
673 }
674
675 update_option( $this->flat_rate_option, $flat_rates );
676
677 $this->get_flat_rates();
678 }
679
680 681 682 683 684 685 686
687 function save_default_costs( $fields ) {
688 $default_cost = ( $_POST['default_cost'] === '' ) ? '' : wc_format_decimal( $_POST['default_cost'] );
689
690 if ( ! strstr( $_POST['default_fee'], '%' ) ) {
691 $default_fee = ( $_POST['default_fee'] === '' ) ? '' : wc_format_decimal( $_POST['default_fee'] );
692 } else {
693 $default_fee = wc_clean( $_POST['default_fee'] );
694 }
695
696 $fields['cost'] = $default_cost;
697 $fields['fee'] = $default_fee;
698
699 return $fields;
700 }
701
702
703 704 705 706 707 708
709 function get_flat_rates() {
710 $this->flat_rates = array_filter( (array) get_option( $this->flat_rate_option ) );
711 }
712
713 }
714