-
Reinhold Kainhofer authoredReinhold Kainhofer authored
rules_shipping.php 23.21 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');
}
if (!class_exists ('plgVmShipmentRules_Shipping')) {
// Only declare the class once...
/** 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 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');
}
/**
* @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=" (".$plugin->rule_name.")";
}
$pluginName = $return . '<span class="' . $this->_type . '_name">' . $plugin->$plugin_name . $rulename.'</span>' . $description;
return $pluginName;
}
/**
* @param VirtueMartCart $cart
* @param $method
* @param $cart_prices
* @return int
*/
function getCosts (VirtueMartCart $cart, $method, $cart_prices) {
if (empty($method->rules)) $this->parseMethodRules($method);
$cartvals = $this->getCartValues ($cart, $cart_prices);
foreach ($method->rules as $r) {
if ($r->matches($cartvals)) {
$method->tax_id = $r->tax_id;
$method->matched_rule = $r;
$method->rule_name = $r->name;
$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');
}
$calculator = calculationHelper::getInstance ();
$value = $calculator->roundInternal ($this->getCosts ($cart, $method, $cart_prices), 'salesPrice');
$_psType = ucfirst ($this->_psType);
$cart_prices[$this->_psType . 'Value'] = $value;
$taxrules = array();
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 ();
}
if (count ($taxrules) > 0) {
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 . 'Tax'] = $cart_prices['salesPrice' . $_psType] - $calculator->roundInternal ($calculator->executeCalculation($taxrules, $cart_prices[$this->_psType . 'Value'], true));
$calculator->setRevert (false);
} else {
$cart_prices['salesPrice' . $_psType] = $calculator->roundInternal ($calculator->executeCalculation ($taxrules, $cart_prices[$this->_psType . 'Value']), 'salesPrice');
$cart_prices[$this->_psType . 'Tax'] = $calculator->roundInternal (($cart_prices['salesPrice' . $_psType] - $cart_prices[$this->_psType . 'Value']), 'salesPrice');
}
$cart_prices[$this->_psType . '_calc_id'] = $taxrules[0]['virtuemart_calc_id'];
} else {
$cart_prices['salesPrice' . $_psType] = $value;
$cart_prices[$this->_psType . 'Tax'] = 0;
$cart_prices[$this->_psType . '_calc_id'] = 0;
}
}
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;
if (class_exists('ShippingRule_Advanced')) {
$method->rules[]=new ShippingRule_Advanced($r, $countries, $tax);
} else {
$method->rules[]=new ShippingRule($r, $countries, $tax);
}
}
}
protected function parseMethodRules (&$method) {
$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 getOrderDimensions (VirtueMartCart $cart) {
/* 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,
'maxwidth' => 0, 'minwidth' => 9999999999,
'maxheight' => 0, 'minheight' => 9999999999,
);
if ($calculated==0) {
$calculated=1;
foreach ($cart->products as $product) {
$volume = $product->product_length * $product->product_width * $product->product_height;
$dimensions['volume'] += $volume * $product->quantity;
$dimensions['maxvolume'] = max ($dimensions['maxvolume'], $volume);
$dimensions['minvolume'] = min ($dimensions['minvolume'], $volume);
$dimensions['maxlength'] = max ($dimensions['maxlength'], $product->product_length);
$dimensions['minlength'] = min ($dimensions['minlength'], $product->product_length);
$dimensions['maxwidth'] = max ($dimensions['maxwidth'], $product->product_width);
$dimensions['minwidth'] = min ($dimensions['minwidth'], $product->product_width);
$dimensions['maxheight'] = max ($dimensions['maxheight'], $product->product_height);
$dimensions['minheight'] = min ($dimensions['minheight'], $product->product_height);
$articles += $product->quantity;
}
}
return $dimensions;
}
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 getCartValues (VirtueMartCart $cart, $cart_prices) {
$orderWeight = $this->getOrderWeight ($cart, $method->weight_unit);
$dimensions = $this->getOrderDimensions ($cart);
$address = (($cart->ST == 0) ? $cart->BT : $cart->ST);
$products = 0;
$articles = 0;
foreach ($cart->products as $product) {
$products += 1;
$articles += $product->quantity;
}
$cartvals = array('weight'=>$orderWeight,
'zip'=>$address['zip'],
'articles'=>$articles,
'products'=>$products,
'amount'=>$cart_prices['salesPrice'],
'country'=>$address['virtuemart_country_id'],
'volume' => $dimensions['volume'],
'maxvolume' => $dimensions['maxvolume'],
'minvolume' => $dimensions['minvolume'],
'maxlength' => $dimensions['maxlength'],
'minlength' => $dimensions['minlength'],
'maxwidth' => $dimensions['maxwidth'],
'minwidth' => $dimensions['minwidth'],
'maxheight' => $dimensions['maxheight'],
'minheight' => $dimensions['minheight']
);
return $cartvals;
}
/**
* @param \VirtueMartCart $cart
* @param int $method
* @param array $cart_prices
* @return bool
*/
protected function checkConditions ($cart, $method, $cart_prices) {
if (empty($method->rules)) $this->parseMethodRules($method);
$cartvals = $this->getCartValues ($cart, $cart_prices);
foreach ($method->rules as $r) {
if ($r->matches($cartvals)) {
$method->matched_rule = $r;
$method->rule_name = $r->name;
return TRUE;
}
}
vmdebug('checkConditions '.$method->name.' does not fit');
return FALSE;
}
/**
* 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);
}
/**
* @author Max Milbers
* @param $data
* @param $table
* @return bool
*/
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;
} else {
// Try to parse all rules (and spit out error) to inform the user:
$method = new StdClass ();
$this->parseMethodRule ($data['rules1'], $data['countries1'], $data['tax_id1'], $method);
$this->parseMethodRule ($data['rules2'], $data['countries2'], $data['tax_id2'], $method);
$this->parseMethodRule ($data['rules3'], $data['countries3'], $data['tax_id3'], $method);
$this->parseMethodRule ($data['rules4'], $data['countries4'], $data['tax_id4'], $method);
$this->parseMethodRule ($data['rules5'], $data['countries5'], $data['tax_id5'], $method);
$this->parseMethodRule ($data['rules6'], $data['countries6'], $data['tax_id6'], $method);
$this->parseMethodRule ($data['rules7'], $data['countries7'], $data['tax_id7'], $method);
$this->parseMethodRule ($data['rules8'], $data['countries8'], $data['tax_id8'], $method);
return $this->setOnTablePluginParams ($name, $id, $table);
}
}
}
class ShippingRule {
var $rulestring = '';
var $countries = array();
var $tax_id = 0;
var $conditions = array();
var $shipping = 0;
var $includes_tax = 0;
var $name = '';
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 handleAssignment ($variable, $value, $rulepart) {
switch ($variable) {
case 'shipping': $this->shipping = $this->parseShippingTerm($value); break;
case 'shippingwithtax': $this->shipping = $this->parseShippingTerm($value); $this->includes_tax = True; break;
case 'name': $this->name = $value; break;
default: JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_UNKNOWN_VARIABLE', $variable, $rulepart), 'error');
}
}
function parseRule($rule) {
$ruleparts=explode(';', $rule);
$operators = array('<', '<=', '=', '>', '>=', '=>', '=<', '<>', '!=', '==');
$op_re='/\s*(<=|=>|>=|=>|<>|!=|==|<|=|>)\s*/';
foreach ($ruleparts as $p) {
$p = trim($p);
if (empty($p)) continue;
$atoms = preg_split ($op_re, $p, -1, PREG_SPLIT_DELIM_CAPTURE);
if (count($atoms)==1) {
$this->shipping = $this->parseShippingTerm($atoms[0]);
} elseif ($atoms[1]=='=') {
$this->handleAssignment (strtolower($atoms[0]), $atoms[2], $p);
} 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], $p), 'error');
$atoms = array();
}
}
}
}
}
function parseShippingTerm($expr) {
/* In the advanced version, shipping cost can be given as a full mathematical expression */
return strtolower($expr);
}
function evaluateTerm ($expr, $vals) {
/* In the advanced version, all conditions and costs can be given as a full mathematical expression */
$e=$expr;
// Strings indicate members of the $vals array (weight, amount, etc.)
if (!is_numeric($e) && isset($vals[$e])) $e=$vals[$e];
if (!is_numeric($e)) {
JFactory::getApplication()->enqueueMessage(JText::sprintf('VMSHIPMENT_RULES_EVALUATE_NONNUMERIC', $e, $this->rulestring), 'error');
$e = 0;
}
return $e;
}
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['country'], $this->countries))
return False;
foreach ($this->conditions as $c) {
$v1=$this->evaluateTerm($c[1], $vals);
$v2=$this->evaluateTerm($c[2], $vals);
// Unknown strings / unevaluatable terms cause the rule to NOT match:
if (!is_numeric($v1) || !is_numeric($v2)) {
return false;
}
$res = false;
switch ($c[0]) {
case '<': $res = ($v1<$v2); break;
case '<=':
case '=<': $res = ($v1<=$v2); break;
case '==': $res = ($v1==$v2); break;
case '!=':
case '<>': $res = ($v1!=$v2); break;
case '>=':
case '=>': $res = ($v1>=$v2); break;
case '>': $res = ($v1>$v2); break;
}
// Immediately return, if this rule does not match
if (!$res) return false;
}
// All conditions match, so return true
return true;
}
function getShippingCosts($vals) {
return $this->calculateShipping($vals);
}
}
}
// No closing tag