rules_shipping_base.php 44.3 KB
Newer Older
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
<?php

defined ('_JEXEC') or die('Restricted access');

/**
 * Shipment plugin for general, rules-based shipments, like regular postal services with complex shipping cost structures
 *
 * @version $Id$
 * @package VirtueMart
 * @subpackage Plugins - shipment
 * @copyright Copyright (C) 2004-2012 VirtueMart Team - All rights reserved.
 * @copyright Copyright (C) 2013 Reinhold Kainhofer, reinhold@kainhofer.com
 * @license http://www.gnu.org/copyleft/gpl.html GNU/GPL, see LICENSE.php
 * VirtueMart is free software. This version may have been modified pursuant
 * to the GNU General Public License, and as distributed it includes or
 * is derivative of works licensed under the GNU General Public License or
 * other free or open source software licenses.
 * See /administrator/components/com_virtuemart/COPYRIGHT.php for copyright notices and details.
 *
 * http://virtuemart.org
 * @author Reinhold Kainhofer, based on the weight_countries shipping plugin by Valerie Isaksen
 *
 */
if (!class_exists ('vmPSPlugin')) {
	require(JPATH_VM_PLUGINS . DS . 'vmpsplugin.php');
}
// Only declare the class once...
Reinhold Kainhofer's avatar
Reinhold Kainhofer committed
28
if (class_exists ('plgVmShipmentRules_Shipping_Base')) {
29
30
	return;
}
31

32

33
34
35
function is_equal($a, $b) {
	if (is_array($a) && is_array($b)) {
		return !array_diff($a, $b) && !array_diff($b, $a);
36
37
	} elseif (is_string($a) && is_string($b)) {
		return strcmp($a,$b) == 0;
38
39
40
41
	} else {
		return $a == $b;
	}
}
42
/** Shipping costs according to general rules.
Reinhold Kainhofer's avatar
Reinhold Kainhofer committed
43
 *  Supported Variables: Weight, ZIP, Amount, Products (1 for each product, even if multiple ordered), Articles
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
 *  Assignable variables: Shipping, Name
 */
class plgVmShipmentRules_Shipping_Base extends vmPSPlugin {

	/**
	 * @param object $subject
	 * @param array  $config
	 */
	function __construct (& $subject, $config) {
		parent::__construct ($subject, $config);

		$this->_loggable = TRUE;
		$this->_tablepkey = 'id';
		$this->_tableId = 'id';
		$this->tableFields = array_keys ($this->getTableSQLFields ());
		$varsToPush = $this->getVarsToPush ();
		$this->setConfigParameterable ($this->_configTableFieldName, $varsToPush);
	}

	/**
	 * Create the table for this plugin if it does not yet exist.
	 *
	 * @author Valérie Isaksen
	 */
	public function getVmPluginCreateTableSQL () {
		return $this->createTableSQL ('Shipment Rules Table');
	}
71
72
	
	public function printWarning($message) {
73
		// Keep track of warning messages, so we don't print them twice:
74
		global $printed_warnings;
75
76
		if (!isset($printed_warnings))
			$printed_warnings = array();
77
78
79
80
81
		if (!in_array($message, $printed_warnings)) {
			JFactory::getApplication()->enqueueMessage($message, 'error');
			$printed_warnings[] = $message;
		}
	}
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

	/**
	 * @return array
	 */
	function getTableSQLFields () {
		$SQLfields = array(
			'id'                           => 'int(1) UNSIGNED NOT NULL AUTO_INCREMENT',
			'virtuemart_order_id'          => 'int(11) UNSIGNED',
			'order_number'                 => 'char(32)',
			'virtuemart_shipmentmethod_id' => 'mediumint(1) UNSIGNED',
			'shipment_name'                => 'varchar(5000)',
			'rule_name'                    => 'varchar(500)',
			'order_weight'                 => 'decimal(10,4)',
			'order_articles'               => 'int(1)',
			'order_products'               => 'int(1)',
			'shipment_weight_unit'         => 'char(3) DEFAULT \'KG\'',
			'shipment_cost'                => 'decimal(10,2)',
			'tax_id'                       => 'smallint(1)'
		);
		return $SQLfields;
	}

	/**
	 * This method is fired when showing the order details in the frontend.
	 * It displays the shipment-specific data.
	 *
	 * @param integer $virtuemart_order_id The order ID
	 * @param integer $virtuemart_shipmentmethod_id The selected shipment method id
	 * @param string  $shipment_name Shipment Name
	 * @return mixed Null for shipments that aren't active, text (HTML) otherwise
	 * @author Valérie Isaksen
	 * @author Max Milbers
	 */
	public function plgVmOnShowOrderFEShipment ($virtuemart_order_id, $virtuemart_shipmentmethod_id, &$shipment_name) {
		$this->onShowOrderFE ($virtuemart_order_id, $virtuemart_shipmentmethod_id, $shipment_name);
	}

	/**
	 * This event is fired after the order has been stored; it gets the shipment method-
	 * specific data.
	 *
	 * @param int    $order_id The order_id being processed
	 * @param object $cart  the cart
	 * @param array  $order The actual order saved in the DB
	 * @return mixed Null when this method was not selected, otherwise true
	 * @author Valerie Isaksen
	 */
	function plgVmConfirmedOrder (VirtueMartCart $cart, $order) {

		if (!($method = $this->getVmPluginMethod ($order['details']['BT']->virtuemart_shipmentmethod_id))) {
			return NULL; // Another method was selected, do nothing
		}
		if (!$this->selectedThisElement ($method->shipment_element)) {
			return FALSE;
		}
		$values['virtuemart_order_id'] = $order['details']['BT']->virtuemart_order_id;
		$values['order_number'] = $order['details']['BT']->order_number;
		$values['virtuemart_shipmentmethod_id'] = $order['details']['BT']->virtuemart_shipmentmethod_id;
		$values['shipment_name'] = $this->renderPluginName ($method);
		$values['rule_name'] = $method->rule_name;
		$values['order_weight'] = $this->getOrderWeight ($cart, $method->weight_unit);
		$values['order_articles'] = $this->getOrderArticles ($cart);
		$values['order_products'] = $this->getOrderProducts ($cart);
		$values['shipment_weight_unit'] = $method->weight_unit;
		$values['shipment_cost'] = $method->cost;
		$values['tax_id'] = $method->tax_id;
		$this->storePSPluginInternalData ($values);

		return TRUE;
	}

	/**
	 * This method is fired when showing the order details in the backend.
	 * It displays the shipment-specific data.
	 * NOTE, this plugin should NOT be used to display form fields, since it's called outside
	 * a form! Use plgVmOnUpdateOrderBE() instead!
	 *
	 * @param integer $virtuemart_order_id The order ID
	 * @param integer $virtuemart_shipmentmethod_id The order shipment method ID
	 * @param object  $_shipInfo Object with the properties 'shipment' and 'name'
	 * @return mixed Null for shipments that aren't active, text (HTML) otherwise
	 * @author Valerie Isaksen
	 */
	public function plgVmOnShowOrderBEShipment ($virtuemart_order_id, $virtuemart_shipmentmethod_id) {
		if (!($this->selectedThisByMethodId ($virtuemart_shipmentmethod_id))) {
			return NULL;
		}
		$html = $this->getOrderShipmentHtml ($virtuemart_order_id);
		return $html;
	}

	/**
	 * @param $virtuemart_order_id
	 * @return string
	 */
	function getOrderShipmentHtml ($virtuemart_order_id) {

		$db = JFactory::getDBO ();
		$q = 'SELECT * FROM `' . $this->_tablename . '` '
			. 'WHERE `virtuemart_order_id` = ' . $virtuemart_order_id;
		$db->setQuery ($q);
		if (!($shipinfo = $db->loadObject ())) {
			vmWarn (500, $q . " " . $db->getErrorMsg ());
			return '';
		}

		if (!class_exists ('CurrencyDisplay')) {
			require(JPATH_VM_ADMINISTRATOR . DS . 'helpers' . DS . 'currencydisplay.php');
		}

		$currency = CurrencyDisplay::getInstance ();
		$tax = ShopFunctions::getTaxByID ($shipinfo->tax_id);
		$taxDisplay = is_array ($tax) ? $tax['calc_value'] . ' ' . $tax['calc_value_mathop'] : $shipinfo->tax_id;
		$taxDisplay = ($taxDisplay == -1) ? JText::_ ('COM_VIRTUEMART_PRODUCT_TAX_NONE') : $taxDisplay;

		$html = '<table class="adminlist">' . "\n";
		$html .= $this->getHtmlHeaderBE ();
		$html .= $this->getHtmlRowBE ('RULES_SHIPPING_NAME', $shipinfo->shipment_name);
		$html .= $this->getHtmlRowBE ('RULES_WEIGHT', $shipinfo->order_weight . ' ' . ShopFunctions::renderWeightUnit ($shipinfo->shipment_weight_unit));
		$html .= $this->getHtmlRowBE ('RULES_ARTICLES', $shipinfo->order_articles . '/' . $shipinfo->order_products);
		$html .= $this->getHtmlRowBE ('RULES_COST', $currency->priceDisplay ($shipinfo->shipment_cost));
		$html .= $this->getHtmlRowBE ('RULES_TAX', $taxDisplay);
		$html .= '</table>' . "\n";

		return $html;
	}
	
	/** Include the rule name in the shipment name */
	protected function renderPluginName ($plugin) {
		$return = '';
		$plugin_name = $this->_psType . '_name';
		$plugin_desc = $this->_psType . '_desc';
		$description = '';
		// 		$params = new JParameter($plugin->$plugin_params);
		// 		$logo = $params->get($this->_psType . '_logos');
		$logosFieldName = $this->_psType . '_logos';
		$logos = $plugin->$logosFieldName;
		if (!empty($logos)) {
			$return = $this->displayLogos ($logos) . ' ';
		}
		if (!empty($plugin->$plugin_desc)) {
223
			$description = '<span class="' . $this->_type . '_description">' . $plugin->$plugin_desc . '</span>';
224
225
226
		}
		$rulename='';
		if (!empty($plugin->rule_name)) {
227
			$rulename=" (".htmlspecialchars($plugin->rule_name).")";
228
		}
229
		$pluginName = $return . '<span class="' . $this->_type . '_name">' . $plugin->$plugin_name . $rulename.'</span>' . $description;
230
231
232
233
234
		return $pluginName;
	}



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
	protected function findMatchingRule (&$cartvals, $method) {
		$result = array("rule"=>Null, "rule_name"=>"", "modifiers"=>array());
		// TODO: Handle modifiers
		foreach ($method->rules as $r) {
			// If the rule is a variable definition, it will NOT match, but modify the $cartvals array for the next rules
			if ($r->matches($cartvals)) {
				$result["rule"] = $r;
				$result["rule_name"] = $r->getRuleName($cartvals);
				return $result;
			}
		}
		// None of the rules matched, so return NULL;
		return NULL;
	}

	/**
	 * @param \VirtueMartCart $cart
	 * @param int             $method
	 * @param array           $cart_prices
	 * @return bool
	 */
	protected function checkConditions ($cart, $method, $cart_prices) {
		if (!isset($method->rules)) $this->parseMethodRules($method);

		$cartvals = $this->getCartValues ($cart, $method, $cart_prices);
		$match = $this->findMatchingRule ($cartvals, $method);
		if ($match) {
			$method->matched_rule = $match["rule"];
			$method->rule_name = $match["rule_name"];
			// If NoShipping is set, this method should NOT offer any shipping at all, so return FALSE, otherwise TRUE
265
			// If the rule has a name, print it as warning (otherwise don't print anything)
266
			if ($method->matched_rule->isNoShipping()) {
267
268
				if (!empty($method->rule_name))
					$this->printWarning(JText::sprintf('VMSHIPMENT_RULES_NOSHIPPING_MESSAGE', $method->rule_name));
269
270
271
272
273
274
275
276
277
278
				vmdebug('checkConditions '.$method->shipment_name.' indicates NoShipping for rule "'.$method->rule_name.'" ('.$method->matched_rule->rulestring.').');
				return FALSE;
			} else {
				return TRUE;
			}
		}
		vmdebug('checkConditions '.$method->shipment_name.' does not fit');
		return FALSE;
	}

279
280
281
282
283
284
285
	/**
	 * @param VirtueMartCart $cart
	 * @param                $method
	 * @param                $cart_prices
	 * @return int
	 */
	function getCosts (VirtueMartCart $cart, $method, $cart_prices) {
286
		if (!isset($method->rules)) $this->parseMethodRules($method);
287
		$cartvals = $this->getCartValues ($cart, $method, $cart_prices);
288
289
290
291
292
293
294
295
296
297
298
		$match = $this->findMatchingRule ($cartvals, $method);
		if ($match) {
			$r = $match["rule"];
			$rulename = $match["rule_name"];
			vmdebug('Rule '.$rulename.' ('.$r->rulestring.') matched.');
			$method->tax_id = $r->tax_id;
			$method->matched_rule = $r;
			$method->rule_name = $rulename;
			$method->cost = $r->getShippingCosts($cartvals);
			$method->includes_tax = $r->includes_tax;
			return $method->cost;
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
		}
		
		vmdebug('getCosts '.$method->name.' does not return shipping costs');
		return 0;
	}

	/**
	 * update the plugin cart_prices (
	 *
	 * @author Valérie Isaksen (original), Reinhold Kainhofer (tax calculations from shippingWithTax)
	 *
	 * @param $cart_prices: $cart_prices['salesPricePayment'] and $cart_prices['paymentTax'] updated. Displayed in the cart.
	 * @param $value :   fee
	 * @param $tax_id :  tax id
	 */

	function setCartPrices (VirtueMartCart $cart, &$cart_prices, $method) {

317

318
319
320
321
		if (!class_exists ('calculationHelper')) {
			require(JPATH_VM_ADMINISTRATOR . DS . 'helpers' . DS . 'calculationh.php');
		}
		$_psType = ucfirst ($this->_psType);
322
		$calculator = calculationHelper::getInstance ();
323

324
325
326
327
328
329
330
331
		$cart_prices[$this->_psType . 'Value'] = $calculator->roundInternal ($this->getCosts ($cart, $method, $cart_prices), 'salesPrice');

		if($this->_psType=='payment'){
			$cartTotalAmountOrig=$this->getCartAmount($cart_prices);
			$cartTotalAmount=($cartTotalAmountOrig + $method->cost_per_transaction) / (1 -($method->cost_percent_total * 0.01));
			$cart_prices[$this->_psType . 'Value'] = $cartTotalAmount - $cartTotalAmountOrig;
		}

332
333

		$taxrules = array();
334
335
336
		if(isset($method->tax_id) and (int)$method->tax_id === -1){

		} else if (!empty($method->tax_id)) {
337
338
339
340
341
342
			$cart_prices[$this->_psType . '_calc_id'] = $method->tax_id;

			$db = JFactory::getDBO ();
			$q = 'SELECT * FROM #__virtuemart_calcs WHERE `virtuemart_calc_id`="' . $method->tax_id . '" ';
			$db->setQuery ($q);
			$taxrules = $db->loadAssocList ();
343

344
345
346
347
348
349
350
351
352
353
354
355
			if(!empty($taxrules) ){
				foreach($taxrules as &$rule){
					if(!isset($rule['subTotal'])) $rule['subTotal'] = 0;
					if(!isset($rule['taxAmount'])) $rule['taxAmount'] = 0;
					$rule['subTotalOld'] = $rule['subTotal'];
					$rule['taxAmountOld'] = $rule['taxAmount'];
					$rule['taxAmount'] = 0;
					$rule['subTotal'] = $cart_prices[$this->_psType . 'Value'];
				}
			}
		} else {
			$taxrules = array_merge($calculator->_cartData['VatTax'],$calculator->_cartData['taxRulesBill']);
356

357
358
			if(!empty($taxrules) ){
				$denominator = 0.0;
359
				foreach($taxrules as &$rule){
360
					//$rule['numerator'] = $rule['calc_value']/100.0 * $rule['subTotal'];
361
362
					if(!isset($rule['subTotal'])) $rule['subTotal'] = 0;
					if(!isset($rule['taxAmount'])) $rule['taxAmount'] = 0;
363
364
365
366
367
368
369
370
371
					$denominator += ($rule['subTotal']-$rule['taxAmount']);
					$rule['subTotalOld'] = $rule['subTotal'];
					$rule['subTotal'] = 0;
					$rule['taxAmountOld'] = $rule['taxAmount'];
					$rule['taxAmount'] = 0;
					//$rule['subTotal'] = $cart_prices[$this->_psType . 'Value'];
				}
				if(empty($denominator)){
					$denominator = 1;
372
373
374
				}

				foreach($taxrules as &$rule){
375
376
					$frac = ($rule['subTotalOld']-$rule['taxAmountOld'])/$denominator;
					$rule['subTotal'] = $cart_prices[$this->_psType . 'Value'] * $frac;
377
					vmdebug('Part $denominator '.$denominator.' $frac '.$frac,$rule['subTotal']);
378
379
				}
			}
380
381
		}

382

383
384
385
386
387
388
		if(empty($method->cost_per_transaction)) $method->cost_per_transaction = 0.0;
		if(empty($method->cost_percent_total)) $method->cost_percent_total = 0.0;

		if (count ($taxrules) > 0 ) {

			// BEGIN_RK_CHANGES
389
			if ($method->includes_tax) {
390

391
392
393
				$cart_prices['salesPrice' . $_psType] = $calculator->roundInternal ($cart_prices[$this->_psType . 'Value'], 'salesPrice');
				// Calculate the tax from the final sales price:
				$calculator->setRevert (true);
394
395
				$cart_prices[$this->_psType . 'Value'] = $calculator->roundInternal ($calculator->executeCalculation($taxrules, $cart_prices[$this->_psType . 'Value'], true));
				$cart_prices[$this->_psType . 'Tax'] = $cart_prices['salesPrice' . $_psType] - $cart_prices[$this->_psType . 'Value'];
396
397
				$calculator->setRevert (false);
			} else {
398
			// END_RK_CHANGES
399
400
401
			$cart_prices['salesPrice' . $_psType] = $calculator->roundInternal ($calculator->executeCalculation ($taxrules, $cart_prices[$this->_psType . 'Value'],true,false), 'salesPrice');
			//vmdebug('I am in '.get_class($this).' and have this rules now',$taxrules,$cart_prices[$this->_psType . 'Value'],$cart_prices['salesPrice' . $_psType]);
			$cart_prices[$this->_psType . 'Tax'] = $calculator->roundInternal (($cart_prices['salesPrice' . $_psType] -  $cart_prices[$this->_psType . 'Value']), 'salesPrice');
402
			// BEGIN_RK_CHANGES
403
			}
404
			// END_RK_CHANGES
405
406
			reset($taxrules);
			$taxrule =  current($taxrules);
407
			$cart_prices[$this->_psType . '_calc_id'] = $taxrule['virtuemart_calc_id'];
408
409
410
411
412
413

			foreach($taxrules as &$rule){
				if(isset($rule['subTotalOld'])) $rule['subTotal'] += $rule['subTotalOld'];
				if(isset($rule['taxAmountOld'])) $rule['taxAmount'] += $rule['taxAmountOld'];
			}

414
		} else {
415
			$cart_prices['salesPrice' . $_psType] = $cart_prices[$this->_psType . 'Value'];
416
417
418
			$cart_prices[$this->_psType . 'Tax'] = 0;
			$cart_prices[$this->_psType . '_calc_id'] = 0;
		}
419
420


421
		return $cart_prices['salesPrice' . $_psType];
422

423
	}
424

425
426
427
428
429
430
431
432
433
434
435
436
437
438
	protected function createMethodRule ($r, $countries, $tax) {
		return new ShippingRule($r, $countries, $tax);
	}

	private function parseMethodRule ($rulestring, $countries, $tax, &$method) {
		$rules1 = preg_split("/(\r\n|\n|\r)/", $rulestring);
		foreach ($rules1 as $r) {
			// Ignore empty lines
			if (empty($r)) continue;
			$method->rules[] = $this->createMethodRule ($r, $countries, $tax);
		}
	}
	
	protected function parseMethodRules (&$method) {
439
		if (!isset($method->rules)) $method->rules = array();
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
		$this->parseMethodRule ($method->rules1, $method->countries1, $method->tax_id1, $method);
		$this->parseMethodRule ($method->rules2, $method->countries2, $method->tax_id2, $method);
		$this->parseMethodRule ($method->rules3, $method->countries3, $method->tax_id3, $method);
		$this->parseMethodRule ($method->rules4, $method->countries4, $method->tax_id4, $method);
		$this->parseMethodRule ($method->rules5, $method->countries5, $method->tax_id5, $method);
		$this->parseMethodRule ($method->rules6, $method->countries6, $method->tax_id6, $method);
		$this->parseMethodRule ($method->rules7, $method->countries7, $method->tax_id7, $method);
		$this->parseMethodRule ($method->rules8, $method->countries8, $method->tax_id8, $method);
	}

	protected function getOrderArticles (VirtueMartCart $cart) {
		/* Cache the value in a static variable and calculate it only once! */
		static $articles = 0;
		if(empty($articles) and count($cart->products)>0){
			foreach ($cart->products as $product) {
				$articles += $product->quantity;
			}
		}
		return $articles;
	}

461
462
463
464
465
466
467
468
469
	protected function getOrderProducts (VirtueMartCart $cart) {
		/* Cache the value in a static variable and calculate it only once! */
		static $products = 0;
		if(empty($products) and count($cart->products)>0){
			$products = count($cart->products);
		}
		return $products;
	}

470
	protected function getOrderDimensions (VirtueMartCart $cart, $length_dimension) {
471
472
473
474
475
		/* Cache the value in a static variable and calculate it only once! */
		static $calculated = 0;
		static $dimensions=array(
			'volume' => 0,
			'maxvolume' => 0, 'minvolume' => 9999999999,
476
477
478
			'maxlength' => 0, 'minlength' => 9999999999, 'totallength' => 0,
			'maxwidth'  => 0, 'minwidth' => 9999999999,  'totalwidth'  => 0,
			'maxheight' => 0, 'minheight' => 9999999999, 'totalheight' => 0,
479
			'maxpackaging' => 0, 'minpackaging' => 9999999999, 'totalpackaging' => 0,
480
		);
481
482
483
		if ($calculated==0) {
			$calculated=1;
			foreach ($cart->products as $product) {
484
485
486
487
488
489
	
				$l = ShopFunctions::convertDimensionUnit ($product->product_length, $product->product_lwh_uom, $length_dimension);
				$w = ShopFunctions::convertDimensionUnit ($product->product_width, $product->product_lwh_uom, $length_dimension);
				$h = ShopFunctions::convertDimensionUnit ($product->product_height, $product->product_lwh_uom, $length_dimension);

				$volume = $l * $w * $h;
490
491
492
				$dimensions['volume'] += $volume * $product->quantity;
				$dimensions['maxvolume'] = max ($dimensions['maxvolume'], $volume);
				$dimensions['minvolume'] = min ($dimensions['minvolume'], $volume);
493
494
				
				$dimensions['totallength'] += $l * $product->quantity;
495
496
				$dimensions['maxlength'] = max ($dimensions['maxlength'], $l);
				$dimensions['minlength'] = min ($dimensions['minlength'], $l);
497
				$dimensions['totalwidth'] += $w * $product->quantity;
498
499
				$dimensions['maxwidth'] = max ($dimensions['maxwidth'], $w);
				$dimensions['minwidth'] = min ($dimensions['minwidth'], $w);
500
				$dimensions['totalheight'] += $h * $product->quantity;
501
502
				$dimensions['maxheight'] = max ($dimensions['maxheight'], $h);
				$dimensions['minheight'] = min ($dimensions['minheight'], $h);
503
504
505
				$dimensions['totalpackaging'] += $product->product_packaging * $product->quantity;
				$dimensions['maxpackaging'] = max ($dimensions['maxpackaging'], $product->product_packaging);
				$dimensions['minpackaging'] = min ($dimensions['minpackaging'], $product->product_packaging);
506
507
			}
		}
508

509
510
		return $dimensions;
	}
511
	
512
	function getOrderWeights (VirtueMartCart $cart, $weight_unit) {
513
514
515
516
517
518
519
520
		static $calculated = 0;
		static $dimensions=array(
			'weight' => 0,
			'maxweight' => 0, 'minweight' => 9999999999,
		);
		if ($calculated==0 && count($cart->products)>0) {
			$calculated = 1;
			foreach ($cart->products as $product) {
521
				$w = ShopFunctions::convertWeigthUnit ($product->product_weight, $product->product_weight_uom, $weight_unit);
522
523
524
525
				$dimensions['maxweight'] = max ($dimensions['maxweight'], $w);
				$dimensions['minweight'] = min ($dimensions['minweight'], $w);
				$dimensions['weight'] += $w * $product->quantity;
			}
526
		}
527
		return $dimensions;
528
529
	}
	
530
531
532
	function getOrderListProperties (VirtueMartCart $cart) {
		$categories = array();
		$vendors = array();
533
		$skus = array();
534
		$manufacturers = array();
535
536
		foreach ($cart->products as $product) {
			$skus[] = $product->product_sku;
537
538
			$categories = array_merge ($categories, $product->categories);
			$vendors[] = $product->virtuemart_vendor_id;
539
540
541
			if ($product->virtuemart_manufacturer_id) {
				$manufacturers[] = $product->virtuemart_manufacturer_id;
			}
542
		}
543
544
545
546
547
548
549
		$categories = array_unique($categories);
		$vendors = array_unique($vendors);
		return array ('skus'=>$skus, 
			      'categories'=>$categories,
			      'vendors'=>$vendors,
			      'manufacturers'=>$manufacturers,
		);
550
551
	}
	
552
	function getOrderCountryState (VirtueMartCart $cart, $address) {
553
554
555
556
		$data = array (
			'countryid' => 0, 'country' => '', 'country2' => '', 'country3' => '',
			'stateid'   => 0, 'state'   => '', 'state2'   => '', 'state3'   => '',
		);
557
558
559
		
		$countriesModel = VmModel::getModel('country');
		if (isset($address['virtuemart_country_id'])) {
560
			$data['countryid'] = $address['virtuemart_country_id'];
561
562
563
564
565
			// The following is a workaround to make sure the cache is invalidated
			// because if some other extension meanwhile called $countriesModel->getCountries,
			// the cache will be modified, but the model's id will not be changes, so the
			// getData call will return the wrong cache.
			$countriesModel->setId(0); 
566
			$countriesModel->setId($address['virtuemart_country_id']);
567
			$country = $countriesModel->getData($address['virtuemart_country_id']);
568
			if (!empty($country)) {
569
570
571
572
				$data['country'] = $country->country_name;
				$data['country2'] = $country->country_2_code;
				$data['country3'] = $country->country_3_code;
			}
573
574
575
		}
		
		$statesModel = VmModel::getModel('state');
576
		if (isset($address['virtuemart_state_id'])) {
577
			$data['stateid'] = $address['virtuemart_state_id'];
578
579
580
581
582
			// The following is a workaround to make sure the cache is invalidated
			// because if some other extension meanwhile called $countriesModel->getCountries,
			// the cache will be modified, but the model's id will not be changes, so the
			// getData call will return the wrong cache.
			$statesModel->setId(0); 
583
			$statesModel->setId($address['virtuemart_state_id']);
584
			$state = $statesModel->getData($address['virtuemart_state_id']);
585
			if (!empty($state)) {
586
587
588
589
				$data['state'] = $state->state_name;
				$data['state2'] = $state->state_2_code;
				$data['state3'] = $state->state_3_code;
			}
590
591
592
593
594
595
		}
		
		return $data;

	}
	
Reinhold Kainhofer's avatar
Reinhold Kainhofer committed
596
	/** Allow child classes to add additional variables for the rules
597
598
599
600
	 */
	protected function addCustomCartValues (VirtueMartCart $cart, $cart_prices, &$values) {
	}
	protected function getCartValues (VirtueMartCart $cart, $method, $cart_prices) {
601
		$address = (($cart->ST == 0 || $cart->STSameAsBT == 1) ? $cart->BT : $cart->ST);
602
		$zip = isset($address['zip'])?trim($address['zip']):'';
Reinhold Kainhofer's avatar
Reinhold Kainhofer committed
603
604
605
606
607
608
609
		$cartvals = array('zip'=>$zip,
				  'zip1'=>substr($zip,0,1),
				  'zip2'=>substr($zip,0,2),
				  'zip3'=>substr($zip,0,3),
				  'zip4'=>substr($zip,0,4),
				  'zip5'=>substr($zip,0,5),
				  'zip6'=>substr($zip,0,6),
610
				  'city'=>isset($address['city'])?trim($address['city']):'',
Reinhold Kainhofer's avatar
Reinhold Kainhofer committed
611
				  'articles'=>$this->getOrderArticles($cart),
612
				  'products'=>$this->getOrderProducts($cart),
613
				  'amount'=>$cart_prices['salesPrice'],
614
615
616
617
618
619
620
621
622
623
624
				  'amountwithtax'=>$cart_prices['salesPrice'],
				  'amountwithouttax'=>$cart_prices['priceWithoutTax'],

				  'baseprice'=>$cart_prices['basePrice'],
				  'basepricewithtax'=>$cart_prices['basePriceWithTax'],
				  'discountedpricewithouttax'=>$cart_prices['discountedPriceWithoutTax'],
				  'salesprice'=>$cart_prices['salesPrice'],
				  'taxamount'=>$cart_prices['taxAmount'],
				  'salespricewithdiscount'=>$cart_prices['salesPriceWithDiscount'],
				  'discountamount'=>$cart_prices['discountAmount'],
				  'pricewithouttax'=>$cart_prices['priceWithoutTax'],
625
			);
626
		
627
628
629
		// Add 'skus', 'categories', 'vendors' variables:
		$cartvals = array_merge ($cartvals, $this->getOrderListProperties ($cart));
		// Add country / state variables:
630
		$cartvals = array_merge ($cartvals, $this->getOrderCountryState ($cart, $address));
631
		// Add Total/Min/Max weight and dimension variables:
632
		$cartvals = array_merge ($cartvals, $this->getOrderWeights ($cart, $method->weight_unit));
633
		$cartvals = array_merge ($cartvals, $this->getOrderDimensions ($cart, $method->length_unit));
634
635
		// Let child classes update the $cartvals array, or add new variables
		$this->addCustomCartValues($cart, $cart_prices, $cartvals);
636
637
638
		// Add the whole list of cart value to the values, so we can print them out as a debug statement!
		$cartvals['values_debug'] = print_r($cartvals,1);
		$cartvals['values'] = $cartvals;
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
		return $cartvals;
	}

	/**
	 * Create the table for this plugin if it does not yet exist.
	 * This functions checks if the called plugin is active one.
	 * When yes it is calling the standard method to create the tables
	 *
	 * @author Valérie Isaksen
	 *
	 */
	function plgVmOnStoreInstallShipmentPluginTable ($jplugin_id) {
		return $this->onStoreInstallPluginTable ($jplugin_id);
	}

	/**
	 * @param VirtueMartCart $cart
	 * @return null
	 */
	public function plgVmOnSelectCheckShipment (VirtueMartCart &$cart) {
		return $this->OnSelectCheck ($cart);
	}

	/**
	 * plgVmDisplayListFE
	 * This event is fired to display the pluginmethods in the cart (edit shipment/payment) for example
	 *
	 * @param object  $cart Cart object
	 * @param integer $selected ID of the method selected
	 * @return boolean True on success, false on failures, null when this plugin was not selected.
	 * On errors, JError::raiseWarning (or JError::raiseError) must be used to set a message.
	 *
	 * @author Valerie Isaksen
	 * @author Max Milbers
	 */
	public function plgVmDisplayListFEShipment (VirtueMartCart $cart, $selected = 0, &$htmlIn) {
		return $this->displayListFE ($cart, $selected, $htmlIn);
	}

	/**
	 * @param VirtueMartCart $cart
	 * @param array          $cart_prices
	 * @param                $cart_prices_name
	 * @return bool|null
	 */
	public function plgVmOnSelectedCalculatePriceShipment (VirtueMartCart $cart, array &$cart_prices, &$cart_prices_name) {
		return $this->onSelectedCalculatePrice ($cart, $cart_prices, $cart_prices_name);
	}

	/**
	 * plgVmOnCheckAutomaticSelected
	 * Checks how many plugins are available. If only one, the user will not have the choice. Enter edit_xxx page
	 * The plugin must check first if it is the correct type
	 *
	 * @author Valerie Isaksen
	 * @param VirtueMartCart cart: the cart object
	 * @return null if no plugin was found, 0 if more then one plugin was found,  virtuemart_xxx_id if only one plugin is found
	 *
	 */
	function plgVmOnCheckAutomaticSelectedShipment (VirtueMartCart $cart, array $cart_prices = array(), &$shipCounter) {
		if ($shipCounter > 1) {
			return 0;
		}
		return $this->onCheckAutomaticSelected ($cart, $cart_prices, $shipCounter);
	}

	/**
	 * This method is fired when showing when priting an Order
	 * It displays the the payment method-specific data.
	 *
	 * @param integer $_virtuemart_order_id The order ID
	 * @param integer $method_id  method used for this order
	 * @return mixed Null when for payment methods that were not selected, text (HTML) otherwise
	 * @author Valerie Isaksen
	 */
	function plgVmonShowOrderPrint ($order_number, $method_id) {
		return $this->onShowOrderPrint ($order_number, $method_id);
	}

	function plgVmDeclarePluginParamsShipment ($name, $id, &$data) {
		return $this->declarePluginParams ('shipment', $name, $id, $data);
	}

722
723
724
725
726
	/* This function is needed in VM 2.0.14 etc. because otherwise the params are not saved */
	function plgVmSetOnTablePluginParamsShipment ($name, $id, &$table) {

		return $this->setOnTablePluginParams ($name, $id, $table);
	}
727

728
729
730
731
	function plgVmDeclarePluginParamsShipmentVM3 (&$data) {
		return $this->declarePluginParams ('shipment', $data);
	}

732
733
734
735
736
737
738
	function plgVmSetOnTablePluginShipment(&$data,&$table){

		$name = $data['shipment_element'];
		$id = $data['shipment_jplugin_id'];

		if (!empty($this->_psType) and !$this->selectedThis ($this->_psType, $name, $id)) {
			return FALSE;
739
740
		}
		if (isset($data['rules1'])) {
741
742
			// Try to parse all rules (and spit out error) to inform the user:
			$method = new StdClass ();
743
744
745
746
747
748
749
750
			$this->parseMethodRule ($data['rules1'], isset($data['countries1'])?$data['countries1']:array(), $data['tax_id1'], $method);
			$this->parseMethodRule ($data['rules2'], isset($data['countries2'])?$data['countries2']:array(), $data['tax_id2'], $method);
			$this->parseMethodRule ($data['rules3'], isset($data['countries3'])?$data['countries3']:array(), $data['tax_id3'], $method);
			$this->parseMethodRule ($data['rules4'], isset($data['countries4'])?$data['countries4']:array(), $data['tax_id4'], $method);
			$this->parseMethodRule ($data['rules5'], isset($data['countries5'])?$data['countries5']:array(), $data['tax_id5'], $method);
			$this->parseMethodRule ($data['rules6'], isset($data['countries6'])?$data['countries6']:array(), $data['tax_id6'], $method);
			$this->parseMethodRule ($data['rules7'], isset($data['countries7'])?$data['countries7']:array(), $data['tax_id7'], $method);
			$this->parseMethodRule ($data['rules8'], isset($data['countries8'])?$data['countries8']:array(), $data['tax_id8'], $method);
751
		}
752
753
		$ret=$this->setOnTablePluginParams ($name, $id, $table);
		return $ret;
754
755
756
757
	}

}

Reinhold Kainhofer's avatar
Reinhold Kainhofer committed
758
if (class_exists ('ShippingRule')) {
759
760
	return;
}
761
762
763
764
765
766
767
768
769

class ShippingRule {
	var $rulestring = '';
	var $countries = array();
	var $tax_id = 0;
	var $conditions = array();
	var $shipping = 0;
	var $includes_tax = 0;
	var $name = '';
770
	var $is_definition = 0;
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
	
	function __construct ($rule, $countries, $tax_id) {
		if (is_array($countries)) {
			$this->countries = $countries;
		} elseif (!empty($countries)) {
			$this->countries[0] = $countries;
		}
		$this->tax_id = $tax_id;
		$this->rulestring = $rule;
		$this->parseRule($rule);
	}
	
	function parseRule($rule) {
		$ruleparts=explode(';', $rule);
		foreach ($ruleparts as $p) {
			$this->parseRulePart($p);
		}
	}
	
	function handleAssignment ($var, $value, $rulepart) {
		switch (strtolower($var)) {
792
			case 'name': $this->name = $value; break;
793
794
			case 'shipping': $this->shipping = $value; $this->includes_tax = False; break;
			case 'shippingwithtax': $this->shipping = $value; $this->includes_tax = True; break;
795
			case 'variable':   // Variable=... is the same as Definition=...
796
			case 'definition': $this->name = strtolower($value); $this->is_definition = True; break;
797
			case 'value': $this->shipping = $value; break; // definition values are also stored in the shipping member!
798
			case 'comment': break; // Completely ignore all comments!
799
			case 'condition': $this->conditions[] = $value; break;
800
801
802
803
804
			default: JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_VARIABLE', $var, $rulepart), 'error');
		}
	}
	
	
805
806
807
808
809
810
811
812
813
814
815
	function tokenize_expression ($expression) {
		// First, extract all strings, delimited by quotes, then all text operators 
		// (OR, AND, in; but make sure we don't capture parts of words, so we need to 
		// use lookbehind/lookahead patterns to exclude OR following another letter 
		// or followed by another letter) and then all arithmetic operators
		$re = '/\s*("[^"]*"|\'[^\']*\'|<=|=>|>=|=<|<>|!=|==|<|=|>)\s*/i';
		$atoms = preg_split($re, $expression, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
		// JFactory::getApplication()->enqueueMessage("TOKENIZING '$expression' returns: <pre>".print_r($atoms,1)."</pre>", 'error');
		return $atoms;
	}
	
816
817
818
819
820
821
	function parseRulePart($rulepart) {
		/* In the basic version, we only split at the comparison operators and assume each term on the LHS and RHS is one variable or constant */
		/* In the advanced version, all conditions and costs can be given as a full mathematical expression */
		/* Both versions create an expression tree, which can be easily evaluated in evaluateTerm */
		$rulepart = trim($rulepart);
		if (empty($rulepart)) return;
822
823
824
825
826
827
828
829
830
831
832
833
834

		
		// Special-case the name assignment, where we don't want to interpret the value as an arithmetic expression!
		if (preg_match('/^\s*(name|variable|definition)\s*=\s*(["\']?)(.*)\2\s*$/i', $rulepart, $matches)) {
			$this->handleAssignment ($matches[1], $matches[3], $rulepart);
			return;
		}

		// Split at all operators:
		$atoms = $this->tokenize_expression ($rulepart);
		
		/* TODO: Starting from here, the advanced plugin is different! */
		$operators = array('<', '<=', '=', '>', '>=', '=>', '=<', '<>', '!=', '==');
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
		if (count($atoms)==1) {
			$this->shipping = $this->parseShippingTerm($atoms[0]);
		} elseif ($atoms[1]=='=') {
			$this->handleAssignment ($atoms[0], $atoms[2], $rulepart);
		} else {
			// Conditions, need at least three atoms!
			while (count($atoms)>1) {
				if (in_array ($atoms[1], $operators)) {
					$this->conditions[] = array($atoms[1], $this->parseShippingTerm($atoms[0]), $this->parseShippingTerm($atoms[2]));
					array_shift($atoms);
					array_shift($atoms);
				} else {
					JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_OPERATOR', $atoms[1], $rulepart), 'error');
					$atoms = array();
				}
			}
		}
	}

	function parseShippingTerm($expr) {
		/* In the advanced version, shipping cost can be given as a full mathematical expression */
856
857
858
859
860
861
862
		// If the shipping term starts with a double quote, it is a string, so don't turn it into lowercase.
		// All other expressions need to be turned into lowercase, because variable names are case-insensitive!
		if (substr($expr, 0, 1) === '"') {
			return $expr;
		} else {
			return strtolower($expr);
		}
863
864
	}
	
865
866
867
868
869
	function evaluateComparison ($terms, $vals) {
		while (count($terms)>2) {
			$res = false;
			switch ($terms[1]) {
				case '<':  $res = ($terms[0] < $terms[2]);  break;
Reinhold Kainhofer's avatar
Reinhold Kainhofer committed
870
				case '<=':
871
				case '=<': $res = ($terms[0] <= $terms[2]); break;
872
				case '==': $res = is_equal($terms[0], $terms[2]); break;
873
874
875
876
877
				case '!=':
				case '<>': $res = ($terms[0] != $terms[2]); break;
				case '>=':
				case '=>': $res = ($terms[0] >= $terms[2]); break;
				case '>':  $res = ($terms[0] >  $terms[2]);  break;
Reinhold Kainhofer's avatar
Reinhold Kainhofer committed
878
				case '~':
879
880
881
882
883
884
885
886
887
					$l=min(strlen($terms[0]), strlen($terms[2]));
					$res = (strncmp ($terms[0], $terms[2], $l) == 0);
					break;
				default:
					JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_OPERATOR', $terms[1], $this->rulestring), 'error');
					$res = false;
			}

			if ($res==false) return false;
888
			// Remove the first operand and the operator from the comparison:
889
890
891
892
893
894
895
			array_shift($terms);
			array_shift($terms);
		}
		if (count($terms)>1) {
			// We do not have the correct number of terms for chained comparisons, i.e. two terms leftover instead of one!
			JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_ERROR', $this->rulestring), 'error');
			return false;
Reinhold Kainhofer's avatar
Reinhold Kainhofer committed
896
		}
897
898
899
900
		// All conditions were fulfilled, so we can return true
		return true;
	}
	
901
902
903
904
905
906
907
908
909
910
911
	function evaluateListFunction ($function, $args) {
		# First make sure that all arguments are actually lists:
		$allarrays = True;
		foreach ($args as $a) {
			$allarrays = $allarrays && is_array($a);
		}
		if (!$allarrays) {
			JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_LISTFUNCTION_ARGS', $function, $this->rulestring), 'error');
			return false;
			
		}
912
		switch ($function) {
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
			case "length":		return count($args[0]); break;
			case "union": 
			case "join":		return call_user_func_array( "array_merge" , $args); break;
			case "complement":	return call_user_func_array( "array_diff" , $args); break;
			case "intersection":	return call_user_func_array( "array_intersect" , $args); break;
			case "issubset":	# Remove all of superset's elements to see if anything else is left: 
						return !array_diff($args[0], $args[1]); break;
			case "contains":	# Remove all of superset's elements to see if anything else is left: 
						# Notice the different argument order compared to issubset!
						return !array_diff($args[1], $args[0]); break;
			case "list_equal":	return array_unique($args[0])==array_unique($args[1]); break;
			default: 
				JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_LISTFUNCTION_UNKNOWN', $function, $this->rulestring), 'error');
				return false;
		}
	}
	
930
931
932
933
934
935
936
937
938
	function evaluateListContainmentFunction ($function, $args) {
		# First make sure that the first argument is a list:
		if (!is_array($args[0])) {
			JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_LISTFUNCTION_CONTAIN_ARGS', $function, $this->rulestring), 'error');
			return false;
		}
		// Extract the array from the args, the $args varialbe will now only contain the elements to be checked:
		$array = array_shift($args);
		switch ($function) {
939
			case "contains_any": // return true if one of the $args is in the $array
940
941
942
943
944
945
					foreach ($args as $a) { 
						if (in_array($a, $array)) 
							return true; 
					}
					return false;
			
946
			case "contains_all": // return false if one of the $args is NOT in the $array
947
948
949
950
951
					foreach ($args as $a) { 
						if (!in_array($a, $array)) 
							return false; 
					}
					return true;
952
953
954
955
956
957
958
959
960
961
962
963
			case "contains_only": // return false if one of the $array elements is NOT in $args
					foreach ($array as $a) {
						if (!in_array($a, $args))
							return false;
					}
					return true;
			case "contains_none": // return false if one of the $args IS in the $array
					foreach ($args as $a) {
						if (in_array($a, $array))
							return false;
					}
					return true;
964
965
966
967
968
969
			default: 
				JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_LISTFUNCTION_UNKNOWN', $function, $this->rulestring), 'error');
				return false;
		}
	}
	
970
971
972
973
	function evaluateFunction ($function, $args) {
		$func = strtolower($function);
		// Functions with no argument:
		if (count($args) == 0) {
974
			$dt = getdate();
975
			switch ($func) {
976
977
978
979
980
981
982
983
				case "second": return $dt['seconds']; break;
				case "minute": return $dt['minutes']; break;
				case "hour":   return $dt['hours']; break;
				case "day":    return $dt['mday']; break;
				case "weekday":return $dt['wday']; break;
				case "month":  return $dt['mon']; break;
				case "year":   return $dt['year']; break;
				case "yearday":return $dt['yday']; break;
984
985
986
987
988
989
990
991
992
			}
		}
		// Functions with exactly one argument:
		if (count($args) == 1) {
			switch ($func) {
				case "round": return round($args[0]); break;
				case "ceil":  return ceil ($args[0]); break;
				case "floor": return floor($args[0]); break;
				case "abs":   return abs($args[0]); break;
993
				case "not":   return !$args[0]; break;
994
				case "print_r": return print_r($args[0],1); break; 
995
996
			}
		}
997
998
999
		if (count($args) == 2) {
			switch ($func) {
				case "digit": return substr($args[0], $args[1]-1, 1); break;
1000
				case "round": return round($args[0]/$args[1])*$args[1]; break;
For faster browsing, not all history is shown. View entire blame