-
Reinhold Kainhofer authoredReinhold Kainhofer authored
rules_shipping_base.php 39.84 KiB
<?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...
if (class_exists ('plgVmShipmentRules_Shipping_Base')) {
return;
}
// Keep track of warning messages, so we don't print them twice:
$printed_warnings = array();
function is_equal($a, $b) {
if (is_array($a) && is_array($b)) {
return !array_diff($a, $b) && !array_diff($b, $a);
} else {
return $a == $b;
}
}
/** Shipping costs according to general rules.
* Supported Variables: Weight, ZIP, Amount, Products (1 for each product, even if multiple ordered), Articles
* 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');
}
public function printWarning($message) {
global $printed_warnings;
if (!in_array($message, $printed_warnings)) {
JFactory::getApplication()->enqueueMessage($message, 'error');
$printed_warnings[] = $message;
}
}
/**
* @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)) {
$description = '<span class="' . $this->_type . '_description">' . $plugin->$plugin_desc . '</span>';
}
$rulename='';
if (!empty($plugin->rule_name)) {
$rulename=" (".htmlspecialchars($plugin->rule_name).")";
}
$pluginName = $return . '<span class="' . $this->_type . '_name">' . $plugin->$plugin_name . $rulename.'</span>' . $description;
return $pluginName;
}
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
if ($method->matched_rule->isNoShipping()) {
$this->printWarning(JText::sprintf('VMSHIPMENT_RULES_NOSHIPPING_MESSAGE', $method->rule_name));
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;
}
/**
* @param VirtueMartCart $cart
* @param $method
* @param $cart_prices
* @return int
*/
function getCosts (VirtueMartCart $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) {
$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;
}
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) {
if (!class_exists ('calculationHelper')) {
require(JPATH_VM_ADMINISTRATOR . DS . 'helpers' . DS . 'calculationh.php');
}
$_psType = ucfirst ($this->_psType);
$calculator = calculationHelper::getInstance ();
$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;
}
$taxrules = array();
if(isset($method->tax_id) and (int)$method->tax_id === -1){
} else if (!empty($method->tax_id)) {
$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 ();
} else {
$taxrules = array();
if(!empty($calculator->_cartData['VatTax']) ){
$taxrules = $calculator->_cartData['VatTax'];
$denominator = 0;
foreach($taxrules as &$rule){
//$rule['numerator'] = $rule['calc_value']/100.0 * $rule['subTotal'];
$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;
}
foreach($taxrules as &$rule){
$frac = ($rule['subTotalOld']-$rule['taxAmountOld'])/$denominator;
$rule['subTotal'] = $cart_prices[$this->_psType . 'Value'] * $frac;
}
} else if(!empty($calculator->_cartData['taxRulesBill']) ){
$taxrules = array_merge($taxrules,$calculator->_cartData['taxRulesBill']);
}
}
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 the taxing via unpublished categories is used, then the rules use the subtotal which is now overriden here
/*if (count ($taxrules) == 1 and isset($taxrules[1]['subTotal'] )) {
$taxrules[1]['subTotal'] = $cart_prices[$this->_psType . 'Value'];
}/*/
if (count ($taxrules) > 0 ) {
// BEGIN_RK_CHANGES
if ($method->includes_tax) {
$cart_prices['salesPrice' . $_psType] = $calculator->roundInternal ($cart_prices[$this->_psType . 'Value'], 'salesPrice');
// Calculate the tax from the final sales price:
$calculator->setRevert (true);
$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'];
$calculator->setRevert (false);
} else {
// END_RK_CHANGES
$cart_prices['salesPrice' . $_psType] = $calculator->roundInternal ($calculator->executeCalculation ($taxrules, $cart_prices[$this->_psType . 'Value'],true,false), 'salesPrice');
$cart_prices[$this->_psType . 'Tax'] = $calculator->roundInternal (($cart_prices['salesPrice' . $_psType] - $cart_prices[$this->_psType . 'Value']), 'salesPrice');
// BEGIN_RK_CHANGES
}
// END_RK_CHANGES
reset($taxrules);
$taxrule = current($taxrules);
$cart_prices[$this->_psType . '_calc_id'] = $taxrule['virtuemart_calc_id'];
foreach($taxrules as &$rule){
if(isset($rule['subTotalOld'])) $rule['subTotal'] += $rule['subTotalOld'];
if(isset($rule['taxAmountOld'])) $rule['taxAmount'] += $rule['taxAmountOld'];
}
} else {
$cart_prices['salesPrice' . $_psType] = $cart_prices[$this->_psType . 'Value'];
$cart_prices[$this->_psType . 'Tax'] = 0;
$cart_prices[$this->_psType . '_calc_id'] = 0;
}
return $cart_prices['salesPrice' . $_psType];
}
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) {
if (!isset($method->rules)) $method->rules = array();
$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;
}
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;
}
protected function getOrderDimensions (VirtueMartCart $cart, $length_dimension) {
/* Cache the value in a static variable and calculate it only once! */
static $calculated = 0;
static $dimensions=array(
'volume' => 0,
'maxvolume' => 0, 'minvolume' => 9999999999,
'maxlength' => 0, 'minlength' => 9999999999, 'totallength' => 0,
'maxwidth' => 0, 'minwidth' => 9999999999, 'totalwidth' => 0,
'maxheight' => 0, 'minheight' => 9999999999, 'totalheight' => 0,
'maxpackaging' => 0, 'minpackaging' => 9999999999, 'totalpackaging' => 0,
);
if ($calculated==0) {
$calculated=1;
foreach ($cart->products as $product) {
$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;
$dimensions['volume'] += $volume * $product->quantity;
$dimensions['maxvolume'] = max ($dimensions['maxvolume'], $volume);
$dimensions['minvolume'] = min ($dimensions['minvolume'], $volume);
$dimensions['totallength'] += $l * $product->quantity;
$dimensions['maxlength'] = max ($dimensions['maxlength'], $l);
$dimensions['minlength'] = min ($dimensions['minlength'], $l);
$dimensions['totalwidth'] += $w * $product->quantity;
$dimensions['maxwidth'] = max ($dimensions['maxwidth'], $w);
$dimensions['minwidth'] = min ($dimensions['minwidth'], $w);
$dimensions['totalheight'] += $h * $product->quantity;
$dimensions['maxheight'] = max ($dimensions['maxheight'], $h);
$dimensions['minheight'] = min ($dimensions['minheight'], $h);
$dimensions['totalpackaging'] += $product->packaging * $product->quantity;
$dimensions['maxpackaging'] = max ($dimensions['maxpackaging'], $product->packaging);
$dimensions['minpackaging'] = min ($dimensions['minpackaging'], $product->packaging);
}
}
return $dimensions;
}
function getOrderWeights (VirtueMartCart $cart, $weight_unit) {
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) {
$w = ShopFunctions::convertWeigthUnit ($product->product_weight, $product->product_weight_uom, $weight_unit);
$dimensions['maxweight'] = max ($dimensions['maxweight'], $w);
$dimensions['minweight'] = min ($dimensions['minweight'], $w);
$dimensions['weight'] += $w * $product->quantity;
}
}
return $dimensions;
}
function getOrderListProperties (VirtueMartCart $cart) {
$categories = array();
$vendors = array();
$skus = array();
$manufacturers = array();
foreach ($cart->products as $product) {
$skus[] = $product->product_sku;
$categories = array_merge ($categories, $product->categories);
$vendors[] = $product->virtuemart_vendor_id;
if ($product->virtuemart_manufacturer_id) {
$manufacturers[] = $product->virtuemart_manufacturer_id;
}
}
$categories = array_unique($categories);
$vendors = array_unique($vendors);
return array ('skus'=>$skus,
'categories'=>$categories,
'vendors'=>$vendors,
'manufacturers'=>$manufacturers,
);
}
function getOrderCountryState (VirtueMartCart $cart, $address) {
$data = array (
'countryid' => 0, 'country' => '', 'country2' => '', 'country3' => '',
'stateid' => 0, 'state' => '', 'state2' => '', 'state3' => '',
);
$countriesModel = VmModel::getModel('country');
if (isset($address['virtuemart_country_id'])) {
$data['countryid'] = $address['virtuemart_country_id'];
$countriesModel->setId($address['virtuemart_country_id']);
$country = $countriesModel->getData();
if (!empty($country)) {
$data['country'] = $country->country_name;
$data['country2'] = $country->country_2_code;
$data['country3'] = $country->country_3_code;
}
}
$statesModel = VmModel::getModel('state');
if (isset($address['virtuemart_state_id'])) {
$data['stateid'] = $address['virtuemart_state_id'];
$statesModel->setId($address['virtuemart_state_id']);
$state = $statesModel->getData();
if (!empty($state)) {
$data['state'] = $state->state_name;
$data['state2'] = $state->state_2_code;
$data['state3'] = $state->state_3_code;
}
}
return $data;
}
/** Allow child classes to add additional variables for the rules
*/
protected function addCustomCartValues (VirtueMartCart $cart, $cart_prices, &$values) {
}
protected function getCartValues (VirtueMartCart $cart, $method, $cart_prices) {
$address = (($cart->ST == 0) ? $cart->BT : $cart->ST);
$zip = trim($address['zip']);
$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),
'articles'=>$this->getOrderArticles($cart),
'products'=>$this->getOrderProducts($cart),
'amount'=>$cart_prices['salesPrice'],
'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'],
);
// Add 'skus', 'categories', 'vendors' variables:
$cartvals = array_merge ($cartvals, $this->getOrderListProperties ($cart));
// Add country / state variables:
$cartvals = array_merge ($cartvals, $this->getOrderCountryState ($cart, $address));
// Add Total/Min/Max weight and dimension variables:
$cartvals = array_merge ($cartvals, $this->getOrderWeights ($cart, $method->weight_unit));
$cartvals = array_merge ($cartvals, $this->getOrderDimensions ($cart, $method->length_unit));
// Let child classes update the $cartvals array, or add new variables
$this->addCustomCartValues($cart, $cart_prices, $cartvals);
// JFactory::getApplication()->enqueueMessage("<pre>cart values: ".print_r($cartvals,1)."</pre>", 'error');
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);
}
/* 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);
}
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;
}
if (isset($data['rules1'])) {
// Try to parse all rules (and spit out error) to inform the user:
$method = new StdClass ();
$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);
}
$ret=$this->setOnTablePluginParams ($name, $id, $table);
return $ret;
}
}
if (class_exists ('ShippingRule')) {
return;
}
class ShippingRule {
var $rulestring = '';
var $countries = array();
var $tax_id = 0;
var $conditions = array();
var $shipping = 0;
var $includes_tax = 0;
var $name = '';
var $is_definition = 0;
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)) {
case 'name': $this->name = $value; break;
case 'shipping': $this->shipping = $value; $this->includes_tax = False; break;
case 'shippingwithtax': $this->shipping = $value; $this->includes_tax = True; break;
case 'variable': // Variable=... is the same as Definition=...
case 'definition': $this->name = strtolower($value); $this->is_definition = True; break;
case 'value': $this->shipping = $value; break; // definition values are also stored in the shipping member!
case 'comment': break; // Completely ignore all comments!
case 'condition': $this->conditions[] = $value; break;
default: JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_VARIABLE', $var, $rulepart), 'error');
}
}
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 */
$operators = array('<', '<=', '=', '>', '>=', '=>', '=<', '<>', '!=', '==');
$op_re='/\s*(<=|=>|>=|=>|<>|!=|==|<|=|>)\s*/';
$rulepart = trim($rulepart);
if (empty($rulepart)) return;
$atoms = preg_split ($op_re, $rulepart, -1, PREG_SPLIT_DELIM_CAPTURE);
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 */
return strtolower($expr);
}
function evaluateComparison ($terms, $vals) {
while (count($terms)>2) {
$res = false;
switch ($terms[1]) {
case '<': $res = ($terms[0] < $terms[2]); break;
case '<=':
case '=<': $res = ($terms[0] <= $terms[2]); break;
case '==': $res = is_equal($terms[0], $terms[2]); break;
case '!=':
case '<>': $res = ($terms[0] != $terms[2]); break;
case '>=':
case '=>': $res = ($terms[0] >= $terms[2]); break;
case '>': $res = ($terms[0] > $terms[2]); break;
case '~':
$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;
// Remove the first operand and the operator from the comparison:
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;
}
// All conditions were fulfilled, so we can return true
return true;
}
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;
}
switch ($func) {
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;
}
}
function evaluateFunction ($function, $args) {
$func = strtolower($function);
// Functions with no argument:
if (count($args) == 0) {
$dt = getdate();
switch ($func) {
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;
}
}
// 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;
case "not": return !$args[0]; break;
case "print_r": return print_r($args[0],1); break;
}
}
if (count($args) == 2) {
switch ($func) {
case "digit": return substr($args[0], $args[1]-1, 1); break;
case "round": return round($args[0]/$args[1])*$args[1]; break;
case "ceil": return ceil($args[0]/$args[1])*$args[1]; break;
case "floor": return floor($args[0]/$args[1])*$args[1]; break;
}
}
if (count($args) == 3) {
switch ($func) {
case "substring": return substr($args[0], $args[1], $args[2]); break;
}
}
// List functions
if (in_array($func, array("length", "complement", "issubset", "contains", "union", "join", "intersection", "list_equal"))) {
return evaluateListFunction ($func, args);
}
// Functions with variable number of args
switch ($func) {
case "max": return max($args); break;
case "min": return min($args); break;
case "list":
case "array": return $args; break;
}
// No known function matches => print an error, return 0
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_FUNCTION', $function, $this->rulestring), 'error');
return 0;
}
function evaluateTerm ($expr, $vals) {
if (is_null($expr)) {
return $expr;
} elseif (is_numeric ($expr)) {
return $expr;
} elseif (is_string ($expr)) {
// Explicit strings are delimited by '...' or "..."
if (($expr[0]=='\'' || $expr[0]=='"') && ($expr[0]==substr($expr,-1)) ) {
return substr($expr,1,-1);
} elseif (array_key_exists(strtolower($expr), $vals)) {
return $vals[strtolower($expr)];
} else {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_VALUE', $expr, $this->rulestring), 'error');
return null;
}
} elseif (is_array($expr)) {
// Operator
$op = array_shift($expr);
$args = array();
$evaluate = true;
if ($op == "FUNCTION") {
$evaluate = false;
}
foreach ($expr as $e) {
$term = $evaluate ? ($this->evaluateTerm($e, $vals)) : $e;
if ($op == 'COMPARISON') {
// For comparisons, we only evaluate every other term (the operators are NOT evaluated!)
$evaluate = !$evaluate;
}
if ($op == "FUNCTION") {
$evaluate = true;
}
if (is_null($term)) return null;
$args[] = $term;
}
$res = false;
switch ($op) {
// Logical operators:
case 'OR': foreach ($args as $a) { $res = ($res || $a); }; break;
case '&&':
case 'AND': $res = true; foreach ($args as $a) { $res = ($res && $a); }; break;
case 'IN': $res = in_array($args[0], $args[1]); break;
// Comparisons:
case '<':
case '<=':
case '=<':
case '==':
case '!=':
case '<>':
case '>=':
case '=>':
case '>':
case '~':
$res = $this->evaluateComparison(array($args[0], $op, $args[1]), $vals); break;
case 'COMPARISON':
$res = $this->evaluateComparison($args, $vals); break;
// Unary operators:
case '.-': $res = -$args[0]; break;
case '.+': $res = $args[0]; break;
// Binary operators
case "+": $res = ($args[0] + $args[1]); break;
case "-": $res = ($args[0] - $args[1]); break;
case "*": $res = ($args[0] * $args[1]); break;
case "/": $res = ($args[0] / $args[1]); break;
case "%": $res = (fmod($args[0], $args[1])); break;
case "^": $res = ($args[0] ^ $args[1]); break;
// Functions:
case "FUNCTION": $func = array_shift($args); $res = $this->evaluateFunction($func, $args); break;
default: $res = false;
}
// JFactory::getApplication()->enqueueMessage("<pre>Result of ".print_r($expr,1)." is $res.</pre>", 'error');
return $res;
} else {
// Neither string nor numeric, nor operator...
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_UNKNOWN_VALUE', $expr, $this->rulestring), 'error');
return null;
}
}
function calculateShipping ($vals) {
return $this->evaluateTerm($this->shipping, $vals);
}
function matches(&$vals) {
// First, check the country, if any conditions are given:
if (count ($this->countries) > 0 && !in_array ($vals['countryid'], $this->countries)) {
// vmdebug('Rule::matches: Country check failed: countryid='.print_r($vals['countryid'],1).', countries are: '.print_r($this->countries,1).'...');
return False;
}
foreach ($this->conditions as $c) {
// All conditions have to match!
$ret = $this->evaluateTerm($c, $vals);
if (is_null($ret) || (!$ret)) {
return false;
}
}
// All conditions match, so return true for rules; For definitions add the variable to the vals
if ($this->is_definition) {
$vals[$this->name] = $this->evaluateTerm($this->shipping, $vals);
// This rule does not specify shipping costs (just modify the cart values!), so return false
return false;
} else {
return true;
}
}
function getRuleName($vals) {
// Replace all {variable} tags in the name by the variables from $vals
$matches=array();
$name=JText::_($this->name);
preg_match_all('/{([A-Za-z0-9_]+)}/', $name, $matches);
foreach ($matches[1] as $m) {
$var=strtolower($m);
if (isset($vals[$var])) {
$name = str_replace("{".$m."}", strval($vals[$var]), $name);
}
}
return $name;
}
function getShippingCosts($vals) {
return $this->calculateShipping($vals);
}
function isNoShipping() {
// NoShipping is set, so if the rule matches, this method should not offer any shipping at all
return (is_string($this->shipping) && (strtolower($this->shipping)=="noshipping"));
}
function isDefinition() {
return $this->is_definition;
}
}
// No closing tag