dolibarr 23.0.3
price_parser.class.php
Go to the documentation of this file.
1<?php
2/* Copyright (C) 2015 Ion Agorria <ion@agorria.com>
3 * Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
4 * Copyright (C) 2025 Frédéric France <frederic.france@free.fr>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
25require_once DOL_DOCUMENT_ROOT.'/core/class/evalmath.class.php';
26require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
27require_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_expression.class.php';
28require_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_global_variable.class.php';
29require_once DOL_DOCUMENT_ROOT.'/product/dynamic_price/class/price_global_variable_updater.class.php';
30require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
31
36{
40 protected $db;
44 public $limit = 100;
48 public $error_parser;
52 public $error_expr;
56 public $special_chr = "#";
60 public $separator_chr = ";";
61
67 public function __construct($db)
68 {
69 $this->db = $db;
70 }
71
77 public function translatedError()
78 {
79 global $langs;
80 $langs->load("errors");
81 /*
82 -No arg
83 9, an unexpected error occurred
84 14, division by zero
85 19, expression not found
86 20, empty expression
87
88 -1 Arg
89 1, cannot assign to constant '%s'
90 2, cannot redefine built-in function '%s'
91 3, undefined variable '%s' in function definition
92 4, illegal character '%s'
93 5, unexpected '%s'
94 8, unexpected operator '%s'
95 10, operator '%s' lacks operand
96 11, expecting '%s'
97 17, undefined variable '%s'
98 21, empty result '%s'
99 22, negative result '%s'
100 24, variable '%s' exists but has no value
101
102 -2 Args
103 6, wrong number of arguments (%s given, %s expected)
104 23, unknown or non set variable '%s' after %s
105
106 -internal errors
107 7, internal error
108 12, internal error
109 13, internal error
110 15, internal error
111 16, internal error
112 18, internal error
113 */
114 if (empty($this->error_parser)) {
115 return $langs->trans("ErrorPriceExpressionUnknown", 0); //this is not supposed to happen
116 }
117 list($code, $info) = $this->error_parser;
118 if (in_array($code, array(9, 14, 19, 20))) { //Errors which have 0 arg
119 return $langs->trans("ErrorPriceExpression".$code);
120 } elseif (in_array($code, array(1, 2, 3, 4, 5, 8, 10, 11, 17, 21, 22))) { //Errors which have 1 arg
121 return $langs->trans("ErrorPriceExpression".$code, (string) $info);
122 } elseif (in_array($code, array(6, 23))) { //Errors which have 2 args
123 return $langs->trans("ErrorPriceExpression".$code, $info[0], $info[1]);
124 } elseif (in_array($code, array(7, 12, 13, 15, 16, 18))) { //Internal errors
125 return $langs->trans("ErrorPriceExpressionInternal", (string) $code);
126 } else { //Unknown errors
127 return $langs->trans("ErrorPriceExpressionUnknown", (string) $code);
128 }
129 }
130
139 public function parseExpression($product, $expression, $values)
140 {
141 global $user, $hookmanager, $extrafields;
142
143 $action = 'PARSEEXPRESSION';
144 $reshook = $hookmanager->executeHooks('doDynamicPrice', array('expression' => &$expression, 'product' => &$product, 'values' => &$values), $this, $action);
145 if ($reshook > 0) {
146 return $hookmanager->resArray['return'];
147 }
148 //Check if empty
149 $expression = trim($expression);
150 if (empty($expression)) {
151 $this->error_parser = array(20, null);
152 return -2;
153 }
154
155 //Accessible product values by expressions
156 $values = array_merge($values, array(
157 "tva_tx" => $product->tva_tx,
158 "localtax1_tx" => $product->localtax1_tx,
159 "localtax2_tx" => $product->localtax2_tx,
160 "weight" => $product->weight,
161 "length" => $product->length,
162 "surface" => $product->surface,
163 "price_min" => $product->price_min,
164 "cost_price" => $product->cost_price,
165 "pmp" => $product->pmp,
166 ));
167
168 // Retrieve all extrafields if not already not know (should not happen)
169 if (! is_object($extrafields)) {
170 $extrafields = new ExtraFields($this->db);
171 $extrafields->fetch_name_optionals_label($product->table_element);
172 }
173
174 $product->fetch_optionals();
175 if (is_array($extrafields->attributes[$product->table_element]['label'])) {
176 foreach ($extrafields->attributes[$product->table_element]['label'] as $key => $label) {
177 $values["extrafield_".$key] = $product->array_options['options_'.$key];
178 }
179 }
180
181 //Process any pending updaters
182 $price_updaters = new PriceGlobalVariableUpdater($this->db);
183 foreach ($price_updaters->listPendingUpdaters() as $entry) {
184 //Schedule the next update by adding current timestamp (secs) + interval (mins)
185 $entry->update_next_update(dol_now() + ($entry->update_interval * 60), $user);
186 //Do processing
187 $res = $entry->process();
188 //Store any error or clear status if OK
189 $entry->update_status($res < 1 ? $entry->error : '', $user);
190 }
191
192 //Get all global values
193 $price_globals = new PriceGlobalVariable($this->db);
194 foreach ($price_globals->listGlobalVariables() as $entry) {
195 $values["global_".$entry->code] = $entry->value;
196 }
197
198 //Remove internal variables
199 unset($values["supplier_id"]);
200
201 //Prepare the lib, parameters and values
202 $em = new EvalMath();
203 $em->suppress_errors = true; //Don't print errors on page
204 $this->error_expr = null;
205 $last_result = null;
206
207 //Fill each variable in expression from values
208 $expression = str_replace("\n", $this->separator_chr, $expression);
209 foreach ($values as $key => $value) {
210 if ($value === null && strpos($expression, $key) !== false) {
211 $this->error_parser = array(24, $key);
212 return -7;
213 }
214 $expression = str_replace($this->special_chr.$key.$this->special_chr, strval($value), $expression);
215 }
216
217 //Check if there is unfilled variable
218 if (strpos($expression, $this->special_chr) !== false) {
219 $data = explode($this->special_chr, $expression);
220 $variable = $this->special_chr.$data[1];
221 if (isset($data[2])) {
222 $variable .= $this->special_chr;
223 }
224 $this->error_parser = array(23, array($variable, $expression));
225 return -6;
226 }
227
228 //Iterate over each expression split by $separator_chr
229 $expressions = explode($this->separator_chr, $expression);
230 $expressions = array_slice($expressions, 0, $this->limit);
231 foreach ($expressions as $expr) {
232 $expr = trim($expr);
233 if (!empty($expr)) {
234 $last_result = $em->evaluate($expr);
235 $this->error_parser = $em->last_error_code;
236 if ($this->error_parser !== null) { //$em->last_error_code is null if no error happened, so just check if error_parser is not null
237 $this->error_expr = $expr;
238 return -3;
239 }
240 }
241 }
242 $vars = $em->vars();
243 if (empty($vars["price"])) {
244 $vars["price"] = $last_result;
245 }
246 if (!isset($vars["price"])) {
247 $this->error_parser = array(21, $expression);
248 return -4;
249 }
250 if ($vars["price"] < 0) {
251 $this->error_parser = array(22, $expression);
252 return -5;
253 }
254 return $vars["price"];
255 }
256
264 public function parseProduct($product, $extra_values = array())
265 {
266 //Get the expression from db
267 $price_expression = new PriceExpression($this->db);
268 $res = $price_expression->fetch($product->fk_price_expression);
269 if ($res < 1) {
270 $this->error_parser = array(19, null);
271 return -1;
272 }
273
274 //Get the supplier min price
275 $productFournisseur = new ProductFournisseur($this->db);
276 $res = $productFournisseur->find_min_price_product_fournisseur($product->id, 0, 0);
277 if ($res < 0) {
278 $this->error_parser = array(25, null);
279 return -1;
280 } elseif ($res == 0) {
281 $supplier_min_price = 0;
282 $supplier_min_price_with_discount = 0;
283 } else {
284 $supplier_min_price = $productFournisseur->fourn_unitprice;
285 $supplier_min_price_with_discount = $productFournisseur->fourn_unitprice_with_discount;
286 }
287
288 //Accessible values by expressions
289 $extra_values = array_merge($extra_values, array(
290 "supplier_min_price" => $supplier_min_price,
291 "supplier_min_price_with_discount" => $supplier_min_price_with_discount,
292 ));
293
294 //Parse the expression and return the price, if not error occurred check if price is higher than min
295 $result = $this->parseExpression($product, $price_expression->expression, $extra_values);
296 if (empty($this->error_parser)) {
297 if ($result < $product->price_min) {
298 $result = $product->price_min;
299 }
300 }
301 return $result;
302 }
303
311 public function parseProductSupplier($product_supplier, $extra_values = array())
312 {
313 //Get the expression from db
314 $price_expression = new PriceExpression($this->db);
315 $res = $price_expression->fetch($product_supplier->fk_supplier_price_expression);
316 if ($res < 1) {
317 $this->error_parser = array(19, null);
318 return -1;
319 }
320
321 //Get the product data (use ignore_expression to avoid possible recursion)
322 $product_supplier->fetch($product_supplier->id, '', '', '', 1);
323
324 //Accessible values by expressions
325 $extra_values = array_merge($extra_values, array(
326 "supplier_quantity" => $product_supplier->fourn_qty,
327 "supplier_tva_tx" => $product_supplier->fourn_tva_tx,
328 ));
329
330 //Parse the expression and return the price
331 return $this->parseExpression($product_supplier, $price_expression->expression, $extra_values);
332 }
333
342 public function testExpression($product_id, $expression, $extra_values = array())
343 {
344 //Get the product data
345 $product = new Product($this->db);
346 $product->fetch($product_id, '', '', '1');
347
348 //Values for product expressions
349 $extra_values = array_merge($extra_values, array(
350 "supplier_min_price" => 1,
351 "supplier_min_price_with_discount" => 2,
352 ));
353
354 //Values for supplier product expressions
355 $extra_values = array_merge($extra_values, array(
356 "supplier_quantity" => 3,
357 "supplier_tva_tx" => 4,
358 ));
359 return $this->parseExpression($product, $expression, $extra_values);
360 }
361}
Class EvalMath.
Class to manage standard extra fields.
Class for accessing price expression table.
Class for accessing price global variables table.
Class for price global variable updaters table.
Class to parse product price expressions.
parseExpression($product, $expression, $values)
Calculates price based on expression.
parseProduct($product, $extra_values=array())
Calculates product price based on product id and associated expression.
parseProductSupplier($product_supplier, $extra_values=array())
Calculates supplier product price based on product supplier price and associated expression.
__construct($db)
Constructor.
testExpression($product_id, $expression, $extra_values=array())
Tests string expression for validity.
translatedError()
Returns translated error.
Class to manage predefined suppliers products.
Class to manage products or services.
dol_now($mode='gmt')
Return date for now.