1<?php 2/* Copyright (C) 2015 Ion Agorria <ion@agorria.com> 3 * 4 * This program is free software; you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation; either version 3 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18/** 19 * \file htdocs/product/dynamic_price/class/price_parser.class.php 20 * \ingroup product 21 * \brief File of class to calculate prices using expression 22 */ 23require_once DOL_DOCUMENT_ROOT.'/core/class/evalmath.class.php'; 24require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php'; 25require_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_expression.class.php'; 26require_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_global_variable.class.php'; 27require_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_global_variable_updater.class.php'; 28require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php'; 29 30/** 31 * Class to parse product price expressions 32 */ 33class PriceParser 34{ 35 protected $db; 36 // Limit of expressions per price 37 public $limit = 100; 38 // The error that occurred when parsing price 39 public $error_parser; 40 // The expression that caused the error 41 public $error_expr; 42 //The special char 43 public $special_chr = "#"; 44 //The separator char 45 public $separator_chr = ";"; 46 47 /** 48 * Constructor 49 * 50 * @param DoliDB $db Database handler 51 */ 52 public function __construct($db) 53 { 54 $this->db = $db; 55 } 56 57 /** 58 * Returns translated error 59 * 60 * @return string Translated error 61 */ 62 public function translatedError() 63 { 64 global $langs; 65 $langs->load("errors"); 66 /* 67 -No arg 68 9, an unexpected error occured 69 14, division by zero 70 19, expression not found 71 20, empty expression 72 73 -1 Arg 74 1, cannot assign to constant '%s' 75 2, cannot redefine built-in function '%s' 76 3, undefined variable '%s' in function definition 77 4, illegal character '%s' 78 5, unexpected '%s' 79 8, unexpected operator '%s' 80 10, operator '%s' lacks operand 81 11, expecting '%s' 82 17, undefined variable '%s' 83 21, empty result '%s' 84 22, negative result '%s' 85 24, variable '%s' exists but has no value 86 87 -2 Args 88 6, wrong number of arguments (%s given, %s expected) 89 23, unknown or non set variable '%s' after %s 90 91 -internal errors 92 7, internal error 93 12, internal error 94 13, internal error 95 15, internal error 96 16, internal error 97 18, internal error 98 */ 99 if (empty($this->error_parser)) { 100 return $langs->trans("ErrorPriceExpressionUnknown", 0); //this is not supposed to happen 101 } 102 list($code, $info) = $this->error_parser; 103 if (in_array($code, array(9, 14, 19, 20))) { //Errors which have 0 arg 104 return $langs->trans("ErrorPriceExpression".$code); 105 } elseif (in_array($code, array(1, 2, 3, 4, 5, 8, 10, 11, 17, 21, 22))) { //Errors which have 1 arg 106 return $langs->trans("ErrorPriceExpression".$code, $info); 107 } elseif (in_array($code, array(6, 23))) { //Errors which have 2 args 108 return $langs->trans("ErrorPriceExpression".$code, $info[0], $info[1]); 109 } elseif (in_array($code, array(7, 12, 13, 15, 16, 18))) { //Internal errors 110 return $langs->trans("ErrorPriceExpressionInternal", $code); 111 } else //Unknown errors 112 { 113 return $langs->trans("ErrorPriceExpressionUnknown", $code); 114 } 115 } 116 117 /** 118 * Calculates price based on expression 119 * 120 * @param Product $product The Product object to get information 121 * @param String $expression The expression to parse 122 * @param array $values Strings to replaces 123 * @return int > 0 if OK, < 1 if KO 124 */ 125 public function parseExpression($product, $expression, $values) 126 { 127 global $user; 128 global $hookmanager; 129 $action = 'PARSEEXPRESSION'; 130 if ($result = $hookmanager->executeHooks('doDynamiPrice', array( 131 'expression' =>$expression, 132 'product' => $product, 133 'values' => $values 134 ), $this, $action)) { 135 return $result; 136 } 137 //Check if empty 138 $expression = trim($expression); 139 if (empty($expression)) { 140 $this->error_parser = array(20, null); 141 return -2; 142 } 143 144 //Accessible product values by expressions 145 $values = array_merge($values, array( 146 "tva_tx" => $product->tva_tx, 147 "localtax1_tx" => $product->localtax1_tx, 148 "localtax2_tx" => $product->localtax2_tx, 149 "weight" => $product->weight, 150 "length" => $product->length, 151 "surface" => $product->surface, 152 "price_min" => $product->price_min, 153 )); 154 155 //Retrieve all extrafield for product and add it to values 156 $extrafields = new ExtraFields($this->db); 157 $extrafields->fetch_name_optionals_label('product', true); 158 $product->fetch_optionals(); 159 if (is_array($extrafields->attributes[$product->table_element]['label'])) { 160 foreach ($extrafields->attributes[$product->table_element]['label'] as $key => $label) { 161 $values["extrafield_".$key] = $product->array_options['options_'.$key]; 162 } 163 } 164 165 //Process any pending updaters 166 $price_updaters = new PriceGlobalVariableUpdater($this->db); 167 foreach ($price_updaters->listPendingUpdaters() as $entry) { 168 //Schedule the next update by adding current timestamp (secs) + interval (mins) 169 $entry->update_next_update(dol_now() + ($entry->update_interval * 60), $user); 170 //Do processing 171 $res = $entry->process(); 172 //Store any error or clear status if OK 173 $entry->update_status($res < 1 ? $entry->error : '', $user); 174 } 175 176 //Get all global values 177 $price_globals = new PriceGlobalVariable($this->db); 178 foreach ($price_globals->listGlobalVariables() as $entry) { 179 $values["global_".$entry->code] = $entry->value; 180 } 181 182 //Remove internal variables 183 unset($values["supplier_id"]); 184 185 //Prepare the lib, parameters and values 186 $em = new EvalMath(); 187 $em->suppress_errors = true; //Don't print errors on page 188 $this->error_expr = null; 189 $last_result = null; 190 191 //Fill each variable in expression from values 192 $expression = str_replace("\n", $this->separator_chr, $expression); 193 foreach ($values as $key => $value) { 194 if ($value === null && strpos($expression, $key) !== false) { 195 $this->error_parser = array(24, $key); 196 return -7; 197 } 198 $expression = str_replace($this->special_chr.$key.$this->special_chr, strval($value), $expression); 199 } 200 201 //Check if there is unfilled variable 202 if (strpos($expression, $this->special_chr) !== false) { 203 $data = explode($this->special_chr, $expression); 204 $variable = $this->special_chr.$data[1]; 205 if (isset($data[2])) { 206 $variable .= $this->special_chr; 207 } 208 $this->error_parser = array(23, array($variable, $expression)); 209 return -6; 210 } 211 212 //Iterate over each expression splitted by $separator_chr 213 $expressions = explode($this->separator_chr, $expression); 214 $expressions = array_slice($expressions, 0, $this->limit); 215 foreach ($expressions as $expr) { 216 $expr = trim($expr); 217 if (!empty($expr)) { 218 $last_result = $em->evaluate($expr); 219 $this->error_parser = $em->last_error_code; 220 if ($this->error_parser !== null) { //$em->last_error_code is null if no error happened, so just check if error_parser is not null 221 $this->error_expr = $expr; 222 return -3; 223 } 224 } 225 } 226 $vars = $em->vars(); 227 if (empty($vars["price"])) { 228 $vars["price"] = $last_result; 229 } 230 if (!isset($vars["price"])) { 231 $this->error_parser = array(21, $expression); 232 return -4; 233 } 234 if ($vars["price"] < 0) { 235 $this->error_parser = array(22, $expression); 236 return -5; 237 } 238 return $vars["price"]; 239 } 240 241 /** 242 * Calculates product price based on product id and associated expression 243 * 244 * @param Product $product The Product object to get information 245 * @param array $extra_values Any aditional values for expression 246 * @return int > 0 if OK, < 1 if KO 247 */ 248 public function parseProduct($product, $extra_values = array()) 249 { 250 //Get the expression from db 251 $price_expression = new PriceExpression($this->db); 252 $res = $price_expression->fetch($product->fk_price_expression); 253 if ($res < 1) { 254 $this->error_parser = array(19, null); 255 return -1; 256 } 257 258 //Get the supplier min price 259 $productFournisseur = new ProductFournisseur($this->db); 260 $res = $productFournisseur->find_min_price_product_fournisseur($product->id, 0, 0); 261 if ($res < 0) { 262 $this->error_parser = array(25, null); 263 return -1; 264 } elseif ($res == 0) { 265 $supplier_min_price = 0; 266 } else { 267 $supplier_min_price = $productFournisseur->fourn_unitprice; 268 } 269 270 //Accessible values by expressions 271 $extra_values = array_merge($extra_values, array( 272 "supplier_min_price" => $supplier_min_price, 273 )); 274 275 //Parse the expression and return the price, if not error occurred check if price is higher than min 276 $result = $this->parseExpression($product, $price_expression->expression, $extra_values); 277 if (empty($this->error_parser)) { 278 if ($result < $product->price_min) { 279 $result = $product->price_min; 280 } 281 } 282 return $result; 283 } 284 285 /** 286 * Calculates supplier product price based on product supplier price and associated expression 287 * 288 * @param ProductFournisseur $product_supplier The Product supplier object to get information 289 * @param array $extra_values Any aditional values for expression 290 * @return int > 0 if OK, < 1 if KO 291 */ 292 public function parseProductSupplier($product_supplier, $extra_values = array()) 293 { 294 //Get the expression from db 295 $price_expression = new PriceExpression($this->db); 296 $res = $price_expression->fetch($product_supplier->fk_supplier_price_expression); 297 if ($res < 1) { 298 $this->error_parser = array(19, null); 299 return -1; 300 } 301 302 //Get the product data (use ignore_expression to avoid possible recursion) 303 $product_supplier->fetch($product_supplier->id, '', '', '', 1); 304 305 //Accessible values by expressions 306 $extra_values = array_merge($extra_values, array( 307 "supplier_quantity" => $product_supplier->fourn_qty, 308 "supplier_tva_tx" => $product_supplier->fourn_tva_tx, 309 )); 310 311 //Parse the expression and return the price 312 return $this->parseExpression($product_supplier, $price_expression->expression, $extra_values); 313 } 314 315 /** 316 * Tests string expression for validity 317 * 318 * @param int $product_id The Product id to get information 319 * @param string $expression The expression to parse 320 * @param array $extra_values Any aditional values for expression 321 * @return int > 0 if OK, < 1 if KO 322 */ 323 public function testExpression($product_id, $expression, $extra_values = array()) 324 { 325 //Get the product data 326 $product = new Product($this->db); 327 $product->fetch($product_id, '', '', 1); 328 329 //Values for product expressions 330 $extra_values = array_merge($extra_values, array( 331 "supplier_min_price" => 1, 332 )); 333 334 //Values for supplier product expressions 335 $extra_values = array_merge($extra_values, array( 336 "supplier_quantity" => 2, 337 "supplier_tva_tx" => 3, 338 )); 339 return $this->parseExpression($product, $expression, $extra_values); 340 } 341} 342