Merge branch 'IonAgorria-dinamic-prices' into develop

This commit is contained in:
Laurent Destailleur 2014-12-03 13:15:59 +01:00
commit 98e91bffca
17 changed files with 1612 additions and 15 deletions

28
htdocs/core/class/html.form.class.php Normal file → Executable file
View File

@ -1906,7 +1906,7 @@ class Form
$sql = "SELECT p.rowid, p.label, p.ref, p.price, p.duration,";
$sql.= " pfp.ref_fourn, pfp.rowid as idprodfournprice, pfp.price as fprice, pfp.quantity, pfp.remise_percent, pfp.remise, pfp.unitprice,";
$sql.= " s.nom as name";
$sql.= " pfp.fk_price_expression, pfp.fk_product, pfp.tva_tx, s.nom as name";
$sql.= " FROM ".MAIN_DB_PREFIX."product as p";
$sql.= " LEFT JOIN ".MAIN_DB_PREFIX."product_fournisseur_price as pfp ON p.rowid = pfp.fk_product";
if ($socid) $sql.= " AND pfp.fk_soc = ".$socid;
@ -1943,6 +1943,7 @@ class Form
$result=$this->db->query($sql);
if ($result)
{
require_once DOL_DOCUMENT_ROOT.'/product/class/priceparser.class.php';
$num = $this->db->num_rows($result);
@ -1987,6 +1988,17 @@ class Form
{
$outqty=$objp->quantity;
$outdiscount=$objp->remise_percent;
if (!empty($objp->fk_price_expression)) {
$priceparser = new PriceParser($this->db);
$price_result = $priceparser->parse_product_supplier($objp->fk_product, $objp->fk_price_expression, $objp->quantity, $objp->tva_tx);
if ($price_result >= 0) {
$objp->fprice = $price_result;
if ($objp->quantity >= 1)
{
$objp->unitprice = $objp->fprice / $objp->quantity;
}
}
}
if ($objp->quantity == 1)
{
$opt.= price($objp->fprice,1,$langs,0,0,-1,$conf->currency)."/";
@ -2075,7 +2087,7 @@ class Form
$sql = "SELECT p.rowid, p.label, p.ref, p.price, p.duration,";
$sql.= " pfp.ref_fourn, pfp.rowid as idprodfournprice, pfp.price as fprice, pfp.quantity, pfp.unitprice,";
$sql.= " s.nom as name";
$sql.= " pfp.fk_price_expression, pfp.fk_product, pfp.tva_tx, s.nom as name";
$sql.= " FROM ".MAIN_DB_PREFIX."product as p";
$sql.= " LEFT JOIN ".MAIN_DB_PREFIX."product_fournisseur_price as pfp ON p.rowid = pfp.fk_product";
$sql.= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON pfp.fk_soc = s.rowid";
@ -2100,6 +2112,7 @@ class Form
}
else
{
require_once DOL_DOCUMENT_ROOT.'/product/class/priceparser.class.php';
$form.= '<option value="0">&nbsp;</option>';
$i = 0;
@ -2114,6 +2127,17 @@ class Form
}
$opt.= '>'.$objp->name.' - '.$objp->ref_fourn.' - ';
if (!empty($objp->fk_price_expression)) {
$priceparser = new PriceParser($this->db);
$price_result = $priceparser->parse_product_supplier($objp->fk_product, $objp->fk_price_expression, $objp->quantity, $objp->tva_tx);
if ($price_result >= 0) {
$objp->fprice = $price_result;
if ($objp->quantity >= 1)
{
$objp->unitprice = $objp->fprice / $objp->quantity;
}
}
}
if ($objp->quantity == 1)
{
$opt.= price($objp->fprice,1,$langs,0,0,-1,$conf->currency)."/";

View File

@ -0,0 +1,122 @@
<?php
/* Copyright (C) 2014 Ion Agorria <ion@agorria.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* \defgroup produit Module dynamic prices
* \brief Module to manage dynamic prices in products
* \file htdocs/core/modules/modDynamicPrices.class.php
* \ingroup produit
* \brief File to describe module to manage dynamic prices in products
*/
include_once DOL_DOCUMENT_ROOT .'/core/modules/DolibarrModules.class.php';
/**
* Class descriptor of DynamicPrices module
*/
class modDynamicPrices extends DolibarrModules
{
/**
* Constructor. Define names, constants, directories, boxes, permissions
*
* @param DoliDB $db Database handler
*/
function __construct($db)
{
$this->db = $db;
$this->numero = 2200;
$this->family = "products";
// Module label (no space allowed), used if translation string 'ModuleXXXName' not found (where XXX is value of numeric property 'numero' of module)
$this->name = preg_replace('/^mod/i','',get_class($this));
$this->description = "Enable the usage of math expressions for prices";
$this->version = 'experimental'; // 'experimental' or 'dolibarr' or version
// Key used in llx_const table to save module status enabled/disabled (where MYMODULE is value of property name of module in uppercase)
$this->const_name = 'MAIN_MODULE_'.strtoupper($this->name);
// Where to store the module in setup page (0=common,1=interface,2=others,3=very specific)
$this->special = 0;
// Name of image file used for this module.
$this->picto='technic';
// Data directories to create when module is enabled
$this->dirs = array();
// Config pages
//-------------
//$this->config_page_url = array("dynamicprices.php@dynamicprices");
// Dependancies
//-------------
$this->depends = array();
$this->requiredby = array();
$this->langfiles = array("other");
// Constantes
//-----------
$this->const = array();
// New pages on tabs
// -----------------
$this->tabs = array();
// Boxes
//------
$this->boxes = array();
// Permissions
//------------
$this->rights = array();
$this->rights_class = 'dynamicprices';
$r=0;
}
/**
* Function called when module is enabled.
* The init function add constants, boxes, permissions and menus (defined in constructor) into Dolibarr database.
* It also creates data directories
*
* @param string $options Options when enabling module ('', 'noboxes')
* @return int 1 if OK, 0 if KO
*/
function init($options='')
{
// Prevent pb of modules not correctly disabled
//$this->remove($options);
$sql = array();
return $this->_init($sql,$options);
}
/**
* Function called when module is disabled.
* Remove from database constants, boxes and permissions from Dolibarr database.
* Data directories are not deleted
*
* @param string $options Options when enabling module ('', 'noboxes')
* @return int 1 if OK, 0 if KO
*/
function remove($options='')
{
$sql = array();
return $this->_remove($sql,$options);
}
}

15
htdocs/fourn/ajax/getSupplierPrices.php Normal file → Executable file
View File

@ -48,7 +48,7 @@ if (! empty($idprod))
$sql = "SELECT p.rowid, p.label, p.ref, p.price, p.duration,";
$sql.= " pfp.ref_fourn,";
$sql.= " pfp.rowid as idprodfournprice, pfp.price as fprice, pfp.remise_percent, pfp.quantity, pfp.unitprice, pfp.charges, pfp.unitcharges,";
$sql.= " s.nom as name";
$sql.= " pfp.fk_price_expression, pfp.tva_tx, s.nom as name";
$sql.= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp";
$sql.= " LEFT JOIN ".MAIN_DB_PREFIX."product as p ON p.rowid = pfp.fk_product";
$sql.= " LEFT JOIN ".MAIN_DB_PREFIX."societe as s ON s.rowid = pfp.fk_soc";
@ -66,11 +66,24 @@ if (! empty($idprod))
if ($num)
{
require_once DOL_DOCUMENT_ROOT.'/product/class/priceparser.class.php';
$i = 0;
while ($i < $num)
{
$objp = $db->fetch_object($result);
if (!empty($objp->fk_price_expression)) {
$priceparser = new PriceParser($db);
$price_result = $priceparser->parse_product_supplier($idprod, $objp->fk_price_expression, $objp->quantity, $objp->tva_tx);
if ($price_result >= 0) {
$objp->fprice = $price_result;
if ($objp->quantity >= 1)
{
$objp->unitprice = $objp->fprice / $objp->quantity;
}
}
}
$price = $objp->fprice * (1 - $objp->remise_percent / 100);
$unitprice = $objp->unitprice * (1 - $objp->remise_percent / 100);

79
htdocs/fourn/class/fournisseur.product.class.php Normal file → Executable file
View File

@ -27,6 +27,7 @@
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.class.php';
require_once DOL_DOCUMENT_ROOT.'/product/class/priceparser.class.php';
/**
@ -55,6 +56,8 @@ class ProductFournisseur extends Product
var $fourn_unitprice;
var $fourn_tva_npr;
var $fk_price_expression;
/**
* Constructor
@ -320,13 +323,14 @@ class ProductFournisseur extends Product
/**
* Loads the price information of a provider
*
* @param int $rowid Line id
* @return int < 0 if KO, 0 if OK but not found, > 0 if OK
* @param int $rowid Line id
* @param int $ignore_expression Ignores the math expression for calculating price and uses the db value instead
* @return int < 0 if KO, 0 if OK but not found, > 0 if OK
*/
function fetch_product_fournisseur_price($rowid)
function fetch_product_fournisseur_price($rowid, $ignore_expression = 0)
{
$sql = "SELECT pfp.rowid, pfp.price, pfp.quantity, pfp.unitprice, pfp.remise_percent, pfp.remise, pfp.tva_tx, pfp.fk_availability,";
$sql.= " pfp.fk_soc, pfp.ref_fourn, pfp.fk_product, pfp.charges, pfp.unitcharges"; // , pfp.recuperableonly as fourn_tva_npr"; FIXME this field not exist in llx_product_fournisseur_price
$sql.= " pfp.fk_soc, pfp.ref_fourn, pfp.fk_product, pfp.charges, pfp.unitcharges, pfp.fk_price_expression"; // , pfp.recuperableonly as fourn_tva_npr"; FIXME this field not exist in llx_product_fournisseur_price
$sql.= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp";
$sql.= " WHERE pfp.rowid = ".$rowid;
@ -351,6 +355,25 @@ class ProductFournisseur extends Product
$this->fk_product = $obj->fk_product;
$this->fk_availability = $obj->fk_availability;
//$this->fourn_tva_npr = $obj->fourn_tva_npr; // FIXME this field not exist in llx_product_fournisseur_price
$this->fk_price_expression = $obj->fk_price_expression;
if (empty($ignore_expression) && !empty($this->fk_price_expression)) {
$priceparser = new PriceParser($this->db);
$price_result = $priceparser->parse_product_supplier($this->fk_product, $this->fk_price_expression, $this->fourn_qty, $this->fourn_tva_tx);
if ($price_result >= 0) {
$this->fourn_price = $price_result;
//recalculation of unitprice, as probably the price changed...
if ($this->fourn_qty!=0)
{
$this->fourn_unitprice = price2num($this->fourn_price/$this->fourn_qty,'MU');
}
else
{
$this->fourn_unitprice="";
}
}
}
return 1;
}
else
@ -379,7 +402,7 @@ class ProductFournisseur extends Product
global $conf;
$sql = "SELECT s.nom as supplier_name, s.rowid as fourn_id,";
$sql.= " pfp.rowid as product_fourn_pri_id, pfp.ref_fourn, pfp.fk_product as product_fourn_id,";
$sql.= " pfp.rowid as product_fourn_pri_id, pfp.ref_fourn, pfp.fk_product as product_fourn_id, pfp.fk_price_expression,";
$sql.= " pfp.price, pfp.quantity, pfp.unitprice, pfp.remise_percent, pfp.remise, pfp.tva_tx, pfp.fk_availability, pfp.charges, pfp.unitcharges, pfp.info_bits";
$sql.= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp";
$sql.= ", ".MAIN_DB_PREFIX."societe as s";
@ -416,6 +439,16 @@ class ProductFournisseur extends Product
$prodfourn->fk_availability = $record["fk_availability"];
$prodfourn->id = $prodid;
$prodfourn->fourn_tva_npr = $record["info_bits"];
$prodfourn->fk_price_expression = $record["fk_price_expression"];
if (!empty($prodfourn->fk_price_expression)) {
$priceparser = new PriceParser($this->db);
$price_result = $priceparser->parse_product_supplier($prodid, $prodfourn->fk_price_expression, $prodfourn->fourn_qty, $prodfourn->fourn_tva_tx);
if ($price_result >= 0) {
$prodfourn->fourn_price = $price_result;
$prodfourn->fourn_unitprice = null; //force recalculation of unitprice, as probably the price changed...
}
}
if (!isset($prodfourn->fourn_unitprice))
{
@ -468,7 +501,7 @@ class ProductFournisseur extends Product
$sql = "SELECT s.nom as supplier_name, s.rowid as fourn_id,";
$sql.= " pfp.rowid as product_fourn_price_id, pfp.ref_fourn,";
$sql.= " pfp.price, pfp.quantity, pfp.unitprice, pfp.tva_tx, pfp.charges, pfp.unitcharges, ";
$sql.= " pfp.remise, pfp.remise_percent";
$sql.= " pfp.remise, pfp.remise_percent, pfp.fk_price_expression";
$sql.= " FROM ".MAIN_DB_PREFIX."societe as s, ".MAIN_DB_PREFIX."product_fournisseur_price as pfp";
$sql.= " WHERE s.entity IN (".getEntity('societe', 1).")";
$sql.= " AND pfp.fk_product = ".$prodid;
@ -495,6 +528,7 @@ class ProductFournisseur extends Product
$this->fourn_tva_tx = $record["tva_tx"];
$this->fourn_id = $record["fourn_id"];
$this->fourn_name = $record["supplier_name"];
$this->fk_price_expression = $record["fk_price_expression"];
$this->id = $prodid;
$this->db->free($resql);
return 1;
@ -506,6 +540,39 @@ class ProductFournisseur extends Product
}
}
/**
* Sets the price expression
*
* @param string $expression Expression
* @return int <0 if KO, >0 if OK
*/
function set_price_expression($expression_id)
{
global $conf;
// Clean parameters
$this->db->begin();
$sql = "UPDATE ".MAIN_DB_PREFIX."product_fournisseur_price";
$sql.= " SET fk_price_expression = ".$expression_id;
$sql.= " WHERE rowid = ".$this->product_fourn_price_id;
dol_syslog(get_class($this)."::set_price_expression", LOG_DEBUG);
$resql = $this->db->query($sql);
if ($resql)
{
$this->db->commit();
return 1;
}
else
{
$this->error=$this->db->error()." sql=".$sql;
$this->db->rollback();
return -1;
}
}
/**
* Display supplier of product
*

View File

@ -0,0 +1,11 @@
evalmath.class.php
==================
Version 1.0
Taken from http://www.phpclasses.org/browse/file/11680.html, cred to Miles Kaufmann
This repository is cloned for two reasons:
1. To allow downloading the code without signing in to phpclasses.org.
2. To add very small improvements to the code.

View File

@ -0,0 +1,393 @@
<?php
/*
================================================================================
EvalMath - PHP Class to safely evaluate math expressions
Copyright (C) 2005 Miles Kaufmann <http://www.twmagic.com/>
================================================================================
NAME
EvalMath - safely evaluate math expressions
SYNOPSIS
include('evalmath.class.php');
$m = new EvalMath;
// basic evaluation:
$result = $m->evaluate('2+2');
// supports: order of operation; parentheses; negation; built-in functions
$result = $m->evaluate('-8(5/2)^2*(1-sqrt(4))-8');
// create your own variables
$m->evaluate('a = e^(ln(pi))');
// or functions
$m->evaluate('f(x,y) = x^2 + y^2 - 2x*y + 1');
// and then use them
$result = $m->evaluate('3*f(42,a)');
DESCRIPTION
Use the EvalMath class when you want to evaluate mathematical expressions
from untrusted sources. You can define your own variables and functions,
which are stored in the object. Try it, it's fun!
METHODS
$m->evalute($expr)
Evaluates the expression and returns the result. If an error occurs,
prints a warning and returns false. If $expr is a function assignment,
returns true on success.
$m->e($expr)
A synonym for $m->evaluate().
$m->vars()
Returns an associative array of all user-defined variables and values.
$m->funcs()
Returns an array of all user-defined functions.
PARAMETERS
$m->suppress_errors
Set to true to turn off warnings when evaluating expressions
$m->last_error
If the last evaluation failed, contains a string describing the error.
(Useful when suppress_errors is on).
$m->last_error_code
If the last evaluation failed, 2 element array with numeric code and extra info
AUTHOR INFORMATION
Copyright 2005, Miles Kaufmann.
LICENSE
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1 Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
class EvalMath {
var $suppress_errors = false;
var $last_error = null;
var $last_error_code = null;
var $v = array('e'=>2.71,'pi'=>3.14); // variables (and constants)
var $f = array(); // user-defined functions
var $vb = array('e', 'pi'); // constants
var $fb = array( // built-in functions
'sin','sinh','arcsin','asin','arcsinh','asinh',
'cos','cosh','arccos','acos','arccosh','acosh',
'tan','tanh','arctan','atan','arctanh','atanh',
'sqrt','abs','ln','log');
function EvalMath() {
// make the variables a little more accurate
$this->v['pi'] = pi();
$this->v['e'] = exp(1);
}
function e($expr) {
return $this->evaluate($expr);
}
function evaluate($expr) {
$this->last_error = null;
$this->last_error_code = null;
$expr = trim($expr);
if (substr($expr, -1, 1) == ';') $expr = substr($expr, 0, strlen($expr)-1); // strip semicolons at the end
//===============
// is it a variable assignment?
if (preg_match('/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches)) {
if (in_array($matches[1], $this->vb)) { // make sure we're not assigning to a constant
return $this->trigger(1, "cannot assign to constant '$matches[1]'", $matches[1]);
}
if (($tmp = $this->pfx($this->nfx($matches[2]))) === false) return false; // get the result and make sure it's good
$this->v[$matches[1]] = $tmp; // if so, stick it in the variable array
return $this->v[$matches[1]]; // and return the resulting value
//===============
// is it a function assignment?
} elseif (preg_match('/^\s*([a-z]\w*)\s*\(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches)) {
$fnn = $matches[1]; // get the function name
if (in_array($matches[1], $this->fb)) { // make sure it isn't built in
return $this->trigger(2, "cannot redefine built-in function '$matches[1]()'", $matches[1]);
}
$args = explode(",", preg_replace("/\s+/", "", $matches[2])); // get the arguments
if (($stack = $this->nfx($matches[3])) === false) return false; // see if it can be converted to postfix
for ($i = 0; $i<count($stack); $i++) { // freeze the state of the non-argument variables
$token = $stack[$i];
if (preg_match('/^[a-z]\w*$/', $token) and !in_array($token, $args)) {
if (array_key_exists($token, $this->v)) {
$stack[$i] = $this->v[$token];
} else {
return $this->trigger(3, "undefined variable '$token' in function definition", $token);
}
}
}
$this->f[$fnn] = array('args'=>$args, 'func'=>$stack);
return true;
//===============
} else {
return $this->pfx($this->nfx($expr)); // straight up evaluation, woo
}
}
function vars() {
$output = $this->v;
unset($output['pi']);
unset($output['e']);
return $output;
}
function funcs() {
$output = array();
foreach ($this->f as $fnn=>$dat)
$output[] = $fnn . '(' . implode(',', $dat['args']) . ')';
return $output;
}
//===================== HERE BE INTERNAL METHODS ====================\\
// Convert infix to postfix notation
function nfx($expr) {
$index = 0;
$stack = new EvalMathStack;
$output = array(); // postfix form of expression, to be passed to pfx()
$expr = trim(strtolower($expr));
$ops = array('+', '-', '*', '/', '^', '_');
$ops_r = array('+'=>0,'-'=>0,'*'=>0,'/'=>0,'^'=>1); // right-associative operator?
$ops_p = array('+'=>0,'-'=>0,'*'=>1,'/'=>1,'_'=>1,'^'=>2); // operator precedence
$expecting_op = false; // we use this in syntax-checking the expression
// and determining when a - is a negation
if (preg_match("/[^\w\s+*^\/()\.,-]/", $expr, $matches)) { // make sure the characters are all good
return $this->trigger(4, "illegal character '{$matches[0]}'", $matches[0]);
}
while(1) { // 1 Infinite Loop ;)
$op = substr($expr, $index, 1); // get the first character at the current index
// find out if we're currently at the beginning of a number/variable/function/parenthesis/operand
$ex = preg_match('/^([a-z]\w*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr($expr, $index), $match);
//===============
if ($op == '-' and !$expecting_op) { // is it a negation instead of a minus?
$stack->push('_'); // put a negation on the stack
$index++;
} elseif ($op == '_') { // we have to explicitly deny this, because it's legal on the stack
return $this->trigger(4, "illegal character '_'", "_"); // but not in the input expression
//===============
} elseif ((in_array($op, $ops) or $ex) and $expecting_op) { // are we putting an operator on the stack?
if ($ex) { // are we expecting an operator but have a number/variable/function/opening parethesis?
$op = '*'; $index--; // it's an implicit multiplication
}
// heart of the algorithm:
while($stack->count > 0 and ($o2 = $stack->last()) and in_array($o2, $ops) and ($ops_r[$op] ? $ops_p[$op] < $ops_p[$o2] : $ops_p[$op] <= $ops_p[$o2])) {
$output[] = $stack->pop(); // pop stuff off the stack into the output
}
// many thanks: http://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail
$stack->push($op); // finally put OUR operator onto the stack
$index++;
$expecting_op = false;
//===============
} elseif ($op == ')' and $expecting_op) { // ready to close a parenthesis?
while (($o2 = $stack->pop()) != '(') { // pop off the stack back to the last (
if (is_null($o2)) return $this->trigger(5, "unexpected ')'", ")");
else $output[] = $o2;
}
if (preg_match("/^([a-z]\w*)\($/", $stack->last(2), $matches)) { // did we just close a function?
$fnn = $matches[1]; // get the function name
$arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you)
$output[] = $stack->pop(); // pop the function and push onto the output
if (in_array($fnn, $this->fb)) { // check the argument count
if($arg_count > 1)
return $this->trigger(6, "wrong number of arguments ($arg_count given, 1 expected)", array($arg_count, 1));
} elseif (array_key_exists($fnn, $this->f)) {
if ($arg_count != count($this->f[$fnn]['args']))
return $this->trigger(6, "wrong number of arguments ($arg_count given, " . count($this->f[$fnn]['args']) . " expected)", array($arg_count, count($this->f[$fnn]['args'])));
} else { // did we somehow push a non-function on the stack? this should never happen
return $this->trigger(7, "internal error");
}
}
$index++;
//===============
} elseif ($op == ',' and $expecting_op) { // did we just finish a function argument?
while (($o2 = $stack->pop()) != '(') {
if (is_null($o2)) return $this->trigger(5, "unexpected ','", ","); // oops, never had a (
else $output[] = $o2; // pop the argument expression stuff and push onto the output
}
// make sure there was a function
if (!preg_match("/^([a-z]\w*)\($/", $stack->last(2), $matches))
return $this->trigger(5, "unexpected ','", ",");
$stack->push($stack->pop()+1); // increment the argument count
$stack->push('('); // put the ( back on, we'll need to pop back to it again
$index++;
$expecting_op = false;
//===============
} elseif ($op == '(' and !$expecting_op) {
$stack->push('('); // that was easy
$index++;
$allow_neg = true;
//===============
} elseif ($ex and !$expecting_op) { // do we now have a function/variable/number?
$expecting_op = true;
$val = $match[1];
if (preg_match("/^([a-z]\w*)\($/", $val, $matches)) { // may be func, or variable w/ implicit multiplication against parentheses...
if (in_array($matches[1], $this->fb) or array_key_exists($matches[1], $this->f)) { // it's a func
$stack->push($val);
$stack->push(1);
$stack->push('(');
$expecting_op = false;
} else { // it's a var w/ implicit multiplication
$val = $matches[1];
$output[] = $val;
}
} else { // it's a plain old var or num
$output[] = $val;
}
$index += strlen($val);
//===============
} elseif ($op == ')') { // miscellaneous error checking
return $this->trigger(5, "unexpected ')'", ")");
} elseif (in_array($op, $ops) and !$expecting_op) {
return $this->trigger(8, "unexpected operator '$op'", $op);
} else { // I don't even want to know what you did to get here
return $this->trigger(9, "an unexpected error occured");
}
if ($index == strlen($expr)) {
if (in_array($op, $ops)) { // did we end with an operator? bad.
return $this->trigger(10, "operator '$op' lacks operand", $op);
} else {
break;
}
}
while (substr($expr, $index, 1) == ' ') { // step the index past whitespace (pretty much turns whitespace
$index++; // into implicit multiplication if no operator is there)
}
}
while (!is_null($op = $stack->pop())) { // pop everything off the stack and push onto output
if ($op == '(') return $this->trigger(11, "expecting ')'", ")"); // if there are (s on the stack, ()s were unbalanced
$output[] = $op;
}
return $output;
}
// evaluate postfix notation
function pfx($tokens, $vars = array()) {
if ($tokens == false) return false;
$stack = new EvalMathStack;
foreach ($tokens as $token) { // nice and easy
// if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on
if (in_array($token, array('+', '-', '*', '/', '^'))) {
if (is_null($op2 = $stack->pop())) return $this->trigger(12, "internal error");
if (is_null($op1 = $stack->pop())) return $this->trigger(13, "internal error");
switch ($token) {
case '+':
$stack->push($op1+$op2); break;
case '-':
$stack->push($op1-$op2); break;
case '*':
$stack->push($op1*$op2); break;
case '/':
if ($op2 == 0) return $this->trigger(14, "division by zero");
$stack->push($op1/$op2); break;
case '^':
$stack->push(pow($op1, $op2)); break;
}
// if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
} elseif ($token == "_") {
$stack->push(-1*$stack->pop());
// if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
} elseif (preg_match("/^([a-z]\w*)\($/", $token, $matches)) { // it's a function!
$fnn = $matches[1];
if (in_array($fnn, $this->fb)) { // built-in function:
if (is_null($op1 = $stack->pop())) return $this->trigger(15, "internal error");
$fnn = preg_replace("/^arc/", "a", $fnn); // for the 'arc' trig synonyms
if ($fnn == 'ln') $fnn = 'log';
eval('$stack->push(' . $fnn . '($op1));'); // perfectly safe eval()
} elseif (array_key_exists($fnn, $this->f)) { // user function
// get args
$args = array();
for ($i = count($this->f[$fnn]['args'])-1; $i >= 0; $i--) {
if (is_null($args[$this->f[$fnn]['args'][$i]] = $stack->pop())) return $this->trigger(16, "internal error");
}
$stack->push($this->pfx($this->f[$fnn]['func'], $args)); // yay... recursion!!!!
}
// if the token is a number or variable, push it on the stack
} else {
if (is_numeric($token)) {
$stack->push($token);
} elseif (array_key_exists($token, $this->v)) {
$stack->push($this->v[$token]);
} elseif (array_key_exists($token, $vars)) {
$stack->push($vars[$token]);
} else {
return $this->trigger(17, "undefined variable '$token'", $token);
}
}
}
// when we're out of tokens, the stack should have a single element, the final result
if ($stack->count != 1) return $this->trigger(18, "internal error");
return $stack->pop();
}
// trigger an error, but nicely, if need be
function trigger($code, $msg, $info = null) {
$this->last_error = $msg;
$this->last_error_code = array($code, $info);
if (!$this->suppress_errors) trigger_error($msg, E_USER_WARNING);
return false;
}
}
// for internal use
class EvalMathStack {
var $stack = array();
var $count = 0;
function push($val) {
$this->stack[$this->count] = $val;
$this->count++;
}
function pop() {
if ($this->count > 0) {
$this->count--;
return $this->stack[$this->count];
}
return null;
}
function last($n=1) {
if (isset($this->stack[$this->count-$n])) {
return $this->stack[$this->count-$n];
}
return;
}
}

View File

@ -0,0 +1,29 @@
--
-- Be carefull to requests order.
-- This file must be loaded by calling /install/index.php page
-- when current version is 3.8.0 or higher.
--
-- To rename a table: ALTER TABLE llx_table RENAME TO llx_table_new;
-- To add a column: ALTER TABLE llx_table ADD COLUMN newcol varchar(60) NOT NULL DEFAULT '0' AFTER existingcol;
-- To rename a column: ALTER TABLE llx_table CHANGE COLUMN oldname newname varchar(60);
-- To drop a column: ALTER TABLE llx_table DROP COLUMN oldname;
-- To change type of field: ALTER TABLE llx_table MODIFY COLUMN name varchar(60);
-- To drop a foreign key: ALTER TABLE llx_table DROP FOREIGN KEY fk_name;
-- To restrict request to Mysql version x.y use -- VMYSQLx.y
-- To restrict request to Pgsql version x.y use -- VPGSQLx.y
-- To make pk to be auto increment (mysql): VMYSQL4.3 ALTER TABLE llx_c_shipment_mode CHANGE COLUMN rowid rowid INTEGER NOT NULL AUTO_INCREMENT;
-- To make pk to be auto increment (postgres): VPGSQL8.2 NOT POSSIBLE. MUST DELETE/CREATE TABLE
-- To set a field as NULL: VPGSQL8.2 ALTER TABLE llx_table ALTER COLUMN name DROP NOT NULL;
-- To set a field as default NULL: VPGSQL8.2 ALTER TABLE llx_table ALTER COLUMN name SET DEFAULT NULL;
-- -- VPGSQL8.2 DELETE FROM llx_usergroup_user WHERE fk_user NOT IN (SELECT rowid from llx_user);
-- -- VMYSQL4.1 DELETE FROM llx_usergroup_user WHERE fk_usergroup NOT IN (SELECT rowid from llx_usergroup);
--create table for price expressions and add column in product supplier
create table llx_price_expression
(
rowid integer AUTO_INCREMENT PRIMARY KEY,
title varchar(20) NOT NULL,
expression varchar(80) NOT NULL
)ENGINE=innodb;
ALTER TABLE llx_product_fournisseur_price ADD fk_price_expression integer DEFAULT NULL;

View File

@ -0,0 +1,24 @@
-- ============================================================================
-- Copyright (C) 2014 Ion agorria <ion@agorria.com>
--
-- This program is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <http://www.gnu.org/licenses/>.
--
-- ============================================================================
create table llx_price_expression
(
rowid integer AUTO_INCREMENT PRIMARY KEY,
title varchar(20) NOT NULL,
expression varchar(80) NOT NULL
)ENGINE=innodb;

View File

@ -39,5 +39,6 @@ create table llx_product_fournisseur_price
tva_tx double(6,3) NOT NULL,
info_bits integer NOT NULL DEFAULT 0,
fk_user integer,
fk_price_expression integer,
import_key varchar(14) -- Import key
)ENGINE=innodb;

2
htdocs/langs/en_US/admin.lang Normal file → Executable file
View File

@ -495,6 +495,8 @@ Module1780Name=Categories
Module1780Desc=Category management (products, suppliers and customers)
Module2000Name=WYSIWYG editor
Module2000Desc=Allow to edit some text area using an advanced editor
Module2200Name=Dynamic Prices
Module2200Desc=Enable the usage of math expressions for prices
Module2300Name=Cron
Module2300Desc=Scheduled task management
Module2400Name=Agenda

18
htdocs/langs/en_US/errors.lang Normal file → Executable file
View File

@ -138,6 +138,24 @@ ErrorMemberNotLinkedToAThirpartyLinkOrCreateFirst=Error, this member is not yet
ErrorThereIsSomeDeliveries=Error, there is some deliveries linked to this shipment. Deletion refused.
ErrorCantDeletePaymentReconciliated=Can't delete a payment that had generated a bank transaction that was conciliated
ErrorCantDeletePaymentSharedWithPayedInvoice=Can't delete a payment shared by at least one invoice with status Payed
ErrorPriceExpression1=Cannot assign to constant '%s'
ErrorPriceExpression2=Cannot redefine built-in function '%s'
ErrorPriceExpression3=Undefined variable '%s' in function definition
ErrorPriceExpression4=Illegal character '%s'
ErrorPriceExpression5=Unexpected '%s'
ErrorPriceExpression6=Wrong number of arguments (%s given, %s expected)
ErrorPriceExpression8=Unexpected operator '%s'
ErrorPriceExpression9=An unexpected error occured
ErrorPriceExpression10=Iperator '%s' lacks operand
ErrorPriceExpression11=Expecting '%s'
ErrorPriceExpression14=Division by zero
ErrorPriceExpression17=Undefined variable '%s'
ErrorPriceExpression19=Expression not found
ErrorPriceExpression20=Empty expression
ErrorPriceExpression21=Empty result '%s'
ErrorPriceExpression22=Negative result '%s'
ErrorPriceExpressionInternal=Internal error '%s'
ErrorPriceExpressionUnknown=Unknown error '%s'
# Warnings
WarningMandatorySetupNotComplete=Mandatory setup parameters are not yet defined

5
htdocs/langs/en_US/products.lang Normal file → Executable file
View File

@ -242,3 +242,8 @@ ForceUpdateChildPriceSoc=Set same price on customer subsidiaries
PriceByCustomerLog=Price by customer log
MinimumPriceLimit=Minimum price can't be lower that %s
MinimumRecommendedPrice=Minimum recommended price is : %s
PriceExpressionEditor=Price expression editor
PriceExpressionSelected=Selected price expression
PriceExpressionEditorHelp="price = 2 + 2" or "2 + 2" for setting the price<br>ExtraFields are variables like "#options_myextrafieldkey# * 2"<br>There are special variables like #quantity# and #tva_tx#<br>Use ; to separate expressions
PriceMode=Price mode
PriceNumeric=Number

View File

@ -0,0 +1,349 @@
<?php
/* Copyright (C) 2007-2012 Laurent Destailleur <eldy@users.sourceforge.net>
* Copyright (C) 2014 Juanjo Menent <jmenent@2byte.es>
/* Copyright (C) 2014 Ion Agorria <ion@agorria.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* \file htdocs/product/class/priceexpresion.class.php
* \ingroup product
* \brief Class for accesing price expression table
*/
/**
* Class for accesing price expression table
*/
class PriceExpression
{
var $db; //!< To store db handler
var $error; //!< To return error code (or message)
var $errors=array(); //!< To return several error codes (or messages)
var $id;
var $title;
var $expression;
/**
* Constructor
*
* @param DoliDb $db Database handler
*/
function __construct($db)
{
$this->db = $db;
return 1;
}
/**
* Create object into database
*
* @param User $user User that creates
* @param int $notrigger 0=launch triggers after, 1=disable triggers
* @return int <0 if KO, Id of created object if OK
*/
function create($user, $notrigger=0)
{
$error=0;
// Clean parameters
if (isset($this->title)) $this->title=trim($this->title);
if (isset($this->expression)) $this->expression=trim($this->expression);
// Insert request
$sql = "INSERT INTO ".MAIN_DB_PREFIX."price_expression (";
$sql.= "title, expression";
$sql.= ") VALUES (";
$sql.= " ".(isset($this->title)?"'".$this->db->escape($this->title)."'":"''").",";
$sql.= " ".(isset($this->expression)?"'".$this->db->escape($this->expression)."'":"''");
$sql.= ")";
$this->db->begin();
dol_syslog(get_class($this)."::create", LOG_DEBUG);
$resql=$this->db->query($sql);
if (! $resql) { $error++; $this->errors[]="Error ".$this->db->lasterror(); }
if (! $error)
{
$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.$this->table_element);
if (! $notrigger)
{
// Uncomment this and change MYOBJECT to your own tag if you
// want this action calls a trigger.
//// Call triggers
//$result=$this->call_trigger('MYOBJECT_CREATE',$user);
//if ($result < 0) { $error++; //Do also what you must do to rollback action if trigger fail}
//// End call triggers
}
}
// Commit or rollback
if ($error)
{
foreach($this->errors as $errmsg)
{
dol_syslog(__METHOD__." ".$errmsg, LOG_ERR);
$this->error.=($this->error?', '.$errmsg:$errmsg);
}
$this->db->rollback();
return -1*$error;
}
else
{
$this->db->commit();
return $this->id;
}
}
/**
* Load object in memory from the database
*
* @param int $id Id object
* @return int < 0 if KO, 0 if OK but not found, > 0 if OK
*/
function fetch($id)
{
$sql = "SELECT title, expression";
$sql.= " FROM ".MAIN_DB_PREFIX."price_expression";
$sql.= " WHERE rowid = ".$id;
dol_syslog(get_class($this)."::fetch");
$resql=$this->db->query($sql);
if ($resql)
{
$obj = $this->db->fetch_object($resql);
if ($obj)
{
$this->id = $id;
$this->title = $obj->title;
$this->expression = $obj->expression;
return 1;
}
else
{
return 0;
}
}
else
{
$this->error="Error ".$this->db->lasterror();
return -1;
}
}
/**
* List all price expressions
*
* @return array Array of price expressions
*/
function list_price_expression()
{
$sql = "SELECT rowid, title, expression";
$sql.= " FROM ".MAIN_DB_PREFIX."price_expression";
$sql.= " ORDER BY title";
dol_syslog(get_class($this)."::list_price_expression");
$resql=$this->db->query($sql);
if ($resql)
{
$retarray = array();
while ($record = $this->db->fetch_array($resql))
{
$price_expression_obj = new PriceExpression($this->db);
$price_expression_obj->id = $record["rowid"];
$price_expression_obj->title = $record["title"];
$price_expression_obj->expression = $record["expression"];
$retarray[]=$price_expression_obj;
}
$this->db->free($resql);
return $retarray;
}
else
{
$this->error=$this->db->error();
return -1;
}
}
/**
* Returns any existing rowid with specified title
*
* @param String $title Title of expression
* @return int < 0 if KO, 0 if OK but not found, > 0 rowid
*/
function find_title($title)
{
$sql = "SELECT rowid";
$sql.= " FROM ".MAIN_DB_PREFIX."price_expression";
$sql.= " WHERE title = '".$this->db->escape($title)."'";
dol_syslog(get_class($this)."::find_title");
$resql=$this->db->query($sql);
if ($resql)
{
$obj = $this->db->fetch_object($resql);
if ($obj)
{
return (int) $obj->rowid;
}
else
{
return 0;
}
}
else
{
$this->error="Error ".$this->db->lasterror();
return -1;
}
}
/**
* Update object into database
*
* @param User $user User that modifies
* @param int $notrigger 0=launch triggers after, 1=disable triggers
* @return int <0 if KO, >0 if OK
*/
function update($user=0, $notrigger=0)
{
$error=0;
// Clean parameters
if (isset($this->title)) $this->title=trim($this->title);
if (isset($this->expression)) $this->expression=trim($this->expression);
// Update request
$sql = "UPDATE ".MAIN_DB_PREFIX."price_expression SET";
$sql.= " title = ".(isset($this->title)?"'".$this->db->escape($this->title)."'":"''").",";
$sql.= " expression = ".(isset($this->expression)?"'".$this->db->escape($this->expression)."'":"''")."";
$sql.= " WHERE rowid = ".$this->id;
$this->db->begin();
dol_syslog(get_class($this)."::update");
$resql = $this->db->query($sql);
if (! $resql) { $error++; $this->errors[]="Error ".$this->db->lasterror(); }
if (! $error)
{
if (! $notrigger)
{
// Uncomment this and change MYOBJECT to your own tag if you
// want this action calls a trigger.
//// Call triggers
//$result=$this->call_trigger('MYOBJECT_MODIFY',$user);
//if ($result < 0) { $error++; //Do also what you must do to rollback action if trigger fail}
//// End call triggers
}
}
// Commit or rollback
if ($error)
{
foreach($this->errors as $errmsg)
{
dol_syslog(get_class($this)."::update ".$errmsg, LOG_ERR);
$this->error.=($this->error?', '.$errmsg:$errmsg);
}
$this->db->rollback();
return -1*$error;
}
else
{
$this->db->commit();
return 1;
}
}
/**
* Delete object in database
*
* @param int $rowid Row id of expression
* @param User $user User that deletes
* @param int $notrigger 0=launch triggers after, 1=disable triggers
* @return int <0 if KO, >0 if OK
*/
function delete($rowid, $user, $notrigger=0)
{
$error=0;
$this->db->begin();
if (! $error)
{
if (! $notrigger)
{
// Uncomment this and change MYOBJECT to your own tag if you
// want this action calls a trigger.
//// Call triggers
//$result=$this->call_trigger('MYOBJECT_DELETE',$user);
//if ($result < 0) { $error++; //Do also what you must do to rollback action if trigger fail}
//// End call triggers
}
}
if (! $error)
{
$sql = "DELETE FROM ".MAIN_DB_PREFIX."price_expression";
$sql.= " WHERE rowid = ".$rowid;
dol_syslog(get_class($this)."::delete");
$resql = $this->db->query($sql);
if (! $resql) { $error++; $this->errors[]="Error ".$this->db->lasterror(); }
}
// Commit or rollback
if ($error)
{
foreach($this->errors as $errmsg)
{
dol_syslog(get_class($this)."::delete ".$errmsg, LOG_ERR);
$this->error.=($this->error?', '.$errmsg:$errmsg);
}
$this->db->rollback();
return -1*$error;
}
else
{
$this->db->commit();
return 1;
}
}
/**
* Initialise object with example values
* Id must be 0 if object instance is a specimen
*
* @return void
*/
function initAsSpecimen()
{
$this->id=0;
$this->expression='';
}
}

View File

@ -0,0 +1,240 @@
<?php
/* Copyright (C) 2014 Ion Agorria <ion@agorria.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* \file htdocs/product/class/priceparser.class.php
* \ingroup product
* \brief File of class to calculate prices using expression
*/
require_once DOL_DOCUMENT_ROOT.'/includes/evalmath/evalmath.class.php';
require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php';
require_once DOL_DOCUMENT_ROOT.'/product/class/priceexpression.class.php';
require_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
/**
* Class to parse product price expressions
*/
class PriceParser
{
protected $db;
// Limit of expressions per price
public $limit = 100;
// The error that ocurred when parsing price
public $error;
// The expression that caused the error
public $error_expr;
//The special char
public $special_chr = "#";
//The separator char
public $separator_chr = ";";
/**
* Constructor
*
* @param DoliDB $db Database handler
*/
function __construct($db)
{
$this->db = $db;
}
/**
* Returns translated error
*
* @return string Translated error
*/
public function translated_error()
{
global $langs;
$langs->load("errors");
/*
-No arg
9, an unexpected error occured
14, division by zero
19, expression not found
20, empty expression
-1 Arg
1, cannot assign to constant '%s'
2, cannot redefine built-in function '%s'
3, undefined variable '%s' in function definition
4, illegal character '%s'
5, unexpected '%s'
8, unexpected operator '%s'
10, operator '%s' lacks operand
11, expecting '%s'
17, undefined variable '%s'
21, empty result '%s'
22, negative result '%s'
-2 Args
6, wrong number of arguments (%s given, %s expected)
-internal errors
7, internal error
12, internal error
13, internal error
15, internal error
16, internal error
18, internal error
*/
if (empty($this->error)) {
return $langs->trans("ErrorPriceExpressionUnknown", 0); //this is not supposed to happen
}
list($code, $info) = $this->error;
if (in_array($code, array(9, 14, 19, 20))) //Errors which have 0 arg
{
return $langs->trans("ErrorPriceExpression".$code);
}
else if (in_array($code, array(1, 2, 3, 4, 5, 8, 10, 11, 17, 21, 22))) //Errors which have 1 arg
{
return $langs->trans("ErrorPriceExpression".$code, $info);
}
else if (in_array($code, array(6))) //Errors which have 2 args
{
return $langs->trans("ErrorPriceExpression".$code, $info[0], $info[1]);
}
else if (in_array($code, array(7, 12, 13, 15, 16, 18))) //Internal errors
{
return $langs->trans("ErrorPriceExpressionInternal", $code);
}
else //Unknown errors
{
return $langs->trans("ErrorPriceExpressionUnknown", $code);
}
}
/**
* Calculates price based on expression
*
* @param array $values Strings to replaces
* @param String $expression The expression to parse
* @return int > 0 if OK, < 1 if KO
*/
public function parse_expression($values, $expression)
{
//Check if empty
$expression = trim($expression);
if (empty($expression))
{
$this->error = array(20, null);
return -1;
}
//Prepare the lib, parameters and values
$em = new EvalMath();
$em->suppress_errors = true; //Don't print errors on page
$this->error_expr = null;
$search = array();
$replace = array();
foreach ($values as $key => $value) {
if ($value !== null) {
$search[] = $this->special_chr.$key.$this->special_chr;
$replace[] = $value;
}
}
//Iterate over each expression splitted by $separator_chr
$expression = str_replace("\n", $this->separator_chr, $expression);
$expressions = explode($this->separator_chr, $expression);
$expressions = array_slice($expressions, 0, $limit);
foreach ($expressions as $expr) {
$expr = trim($expr);
if (!empty($expr))
{
$expr = str_ireplace($search, $replace, $expr);
$last_result = $em->evaluate($expr);
$this->error = $em->last_error_code;
if ($this->error !== null) { //$em->last_error is null if no error happened, so just check if error is not null
$this->error_expr = $expr;
return -2;
}
}
}
$vars = $em->vars();
if (empty($vars["price"])) {
$vars["price"] = $last_result;
}
if ($vars["price"] === null)
{
$this->error = array(21, $expression);
return -3;
}
if ($vars["price"] < 0)
{
$this->error = array(22, $expression);
return -4;
}
return $vars["price"];
}
/**
* Calculates supplier product price based on product id and string expression
*
* @param int $product The Product id to get information
* @param string $expression The expression to parse
* @param int $quantity Min quantity
* @param int $tva_tx VAT rate
* @param array $extra_values Any aditional values for expression
* @return int > 0 if OK, < 1 if KO
*/
public function parse_product_supplier_expression($product_id, $expression, $quantity = null, $tva_tx = null, $extra_values = array())
{
//Accessible values by expressions
$expression_values = array(
"quantity" => $quantity,
"tva_tx" => $tva_tx,
);
$expression_values = array_merge($expression_values, $extra_values);
//Retreive all extrafield for product and add it to expression_values
$extrafields = new ExtraFields($this->db);
$extralabels = $extrafields->fetch_name_optionals_label('product', true);
$product = new Product($this->db);
$product->fetch_optionals($product_id, $extralabels);
foreach($extrafields->attribute_label as $key=>$label)
{
$expression_values['options_'.$key] = $product->array_options['options_'.$key];
}
//Parse the expression and return the price
return $this->parse_expression($expression_values, $expression);
}
/**
* Calculates supplier product price based on product id and expression id
*
* @param int $product The Product id to get information
* @param int $expression_id The expression to parse
* @param int $quantity Min quantity
* @param int $tva_tx VAT rate
* @param array $extra_values Any aditional values for expression
* @return int > 0 if OK, < 1 if KO
*/
public function parse_product_supplier($product_id, $expression_id, $quantity = null, $tva_tx = null, $extra_values = array())
{
$price_expression = new PriceExpression($this->db);
$res = $price_expression->fetch($expression_id);
if ($res > 1) {
$this->error = array(19, null);
return -1;
}
//Parse the expression and return the price
return $this->parse_product_supplier_expression($product_id, $price_expression->expression, $quantity, $tva_tx, $extra_values);
}
}

19
htdocs/product/class/product.class.php Normal file → Executable file
View File

@ -1166,11 +1166,12 @@ class Product extends CommonObject
*/
function get_buyprice($prodfournprice,$qty,$product_id=0,$fourn_ref=0)
{
require_once DOL_DOCUMENT_ROOT.'/product/class/priceparser.class.php';
$result = 0;
// We do select by searching with qty and prodfournprice
$sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity,";
$sql.= " pfp.fk_product, pfp.ref_fourn, pfp.fk_soc, pfp.tva_tx";
$sql.= " pfp.fk_product, pfp.ref_fourn, pfp.fk_soc, pfp.tva_tx, pfp.fk_price_expression";
$sql.= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp";
$sql.= " WHERE pfp.rowid = ".$prodfournprice;
if ($qty) $sql.= " AND pfp.quantity <= ".$qty;
@ -1182,6 +1183,13 @@ class Product extends CommonObject
$obj = $this->db->fetch_object($resql);
if ($obj && $obj->quantity > 0) // If found
{
if (!empty($obj->fk_price_expression)) {
$priceparser = new PriceParser($this->db);
$price_result = $priceparser->parse_product_supplier($obj->fk_product, $obj->fk_price_expression, $obj->quantity, $obj->tva_tx);
if ($price_result >= 0) {
$obj->price = $price_result;
}
}
$this->buyprice = $obj->price; // \deprecated
$this->fourn_pu = $obj->price / $obj->quantity; // Prix unitaire du produit pour le fournisseur $fourn_id
$this->ref_fourn = $obj->ref_fourn; // Ref supplier
@ -1193,7 +1201,7 @@ class Product extends CommonObject
{
// We do same select again but searching with qty, ref and id product
$sql = "SELECT pfp.rowid, pfp.price as price, pfp.quantity as quantity, pfp.fk_soc,";
$sql.= " pfp.fk_product, pfp.ref_fourn as ref_supplier, pfp.tva_tx";
$sql.= " pfp.fk_product, pfp.ref_fourn as ref_supplier, pfp.tva_tx, pfp.fk_price_expression";
$sql.= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pfp";
$sql.= " WHERE pfp.ref_fourn = '".$fourn_ref."'";
$sql.= " AND pfp.fk_product = ".$product_id;
@ -1208,6 +1216,13 @@ class Product extends CommonObject
$obj = $this->db->fetch_object($resql);
if ($obj && $obj->quantity > 0) // If found
{
if (!empty($obj->fk_price_expression)) {
$priceparser = new PriceParser($this->db);
$price_result = $priceparser->parse_product_supplier($obj->fk_product, $obj->fk_price_expression, $obj->quantity, $obj->tva_tx);
if ($result >= 0) {
$obj->price = $price_result;
}
}
$this->buyprice = $obj->price; // deprecated
$this->fourn_qty = $obj->quantity; // min quantity for price
$this->fourn_pu = $obj->price / $obj->quantity; // Prix unitaire du produit pour le fournisseur $fourn_id

221
htdocs/product/expression.php Executable file
View File

@ -0,0 +1,221 @@
<?php
/* Copyright (C) 2014 Ion Agorria <ion@agorria.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* \file htdocs/product/expression.php
* \ingroup product
* \brief Page for editing expression
*/
require '../main.inc.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
require_once DOL_DOCUMENT_ROOT.'/product/class/priceexpression.class.php';
require_once DOL_DOCUMENT_ROOT.'/product/class/priceparser.class.php';
$langs->load("products");
$langs->load("accountancy"); //"Back" translation is on this file
$id = GETPOST('id', 'int');
$eid = GETPOST('eid', 'int');
$action = GETPOST('action', 'alpha');
$title = GETPOST('expression_title', 'alpha');
$expression = GETPOST('expression');
$tab = GETPOST('tab', 'alpha');
$tab = (!empty($tab)) ? $tab : 'card';
// Security check
$result=restrictedArea($user,'produit|service&fournisseur',$id,'product&product','','','rowid');
//Initialize objects
$product = new Product($db);
$product->fetch($id, '');
$price_expression = new PriceExpression($db);
//Fetch expression data
if (empty($eid)) //This also disables fetch when eid == 0
{
$eid = 0;
}
else if ($action != 'delete')
{
$price_expression->fetch($eid);
}
/*
* Actions
*/
if ($action == 'add')
{
if ($eid == 0)
{
$result = $price_expression->find_title($title);
if ($result == 0) //No existing entry found with title, ok
{
//Check the expression validity by parsing it
$priceparser = new PriceParser($db);
$price_result = $priceparser->parse_product_supplier_expression($id, $expression, 0, 0);
if ($price_result < 0) { //Expression is not valid
setEventMessage($priceparser->translated_error(), 'errors');
}
else
{
$price_expression->title = $title;
$price_expression->expression = $expression;
$result = $price_expression->create($user);
if ($result > 0) //created successfully, set the eid to newly created entry
{
$eid = $price_expression->id;
}
else
{
setEventMessage("add: ".$price_expression->error, 'errors');
}
}
}
else if ($result < 0)
{
setEventMessage("add find: ".$price_expression->error, 'errors');
}
else
{
setEventMessage($langs->trans("ErrorRecordAlreadyExists"), 'errors');
}
}
}
if ($action == 'update')
{
if ($eid != 0)
{
$result = $price_expression->find_title($title);
if ($result == 0 || $result == $eid) //No existing entry found with title or existing one is the current one, ok
{
//Check the expression validity by parsing it
$priceparser = new PriceParser($db);
$price_result = $priceparser->parse_product_supplier_expression($id, $expression, 0, 0);
if ($price_result < 0) { //Expression is not valid
setEventMessage($priceparser->translated_error(), 'errors');
}
else
{
$price_expression->id = $eid;
$price_expression->title = $title;
$price_expression->expression = $expression;
$result = $price_expression->update($user);
if ($result < 0)
{
setEventMessage("update: ".$price_expression->error, 'errors');
}
}
}
else if ($result < 0)
{
setEventMessage("update find: ".$price_expression->error, 'errors');
}
else
{
setEventMessage($langs->trans("ErrorRecordAlreadyExists"), 'errors');
}
}
}
if ($action == 'delete')
{
if ($eid != 0)
{
$result = $price_expression->delete($eid, $user);
if ($result < 0)
{
setEventMessage("delete: ".$price_expression->error, 'errors');
}
$eid = 0;
}
}
/*
* View
*/
//Header
llxHeader("","",$langs->trans("CardProduct".$product->type));
print_fiche_titre($langs->trans("PriceExpressionEditor"));
$form = new Form($db);
//Form/Table
print '<form action="'.$_SERVER['PHP_SELF'].'?id='.$id.'&amp;tab='.$tab.'&amp;eid='.$eid.'" method="POST">';
print '<input type="hidden" name="token" value="'.$_SESSION['newtoken'].'">';
print '<input type="hidden" name="action" value='.($eid == 0 ? 'add' : 'update').'>';
print '<table class="border" width="100%">';
// Price expression selector
print '<tr><td class="fieldrequired">'.$langs->trans("PriceExpressionSelected").'</td><td>';
$price_expression_list = array(0 => $langs->trans("New")); //Put the new as first option
foreach ($price_expression->list_price_expression() as $entry) {
$price_expression_list[$entry->id] = $entry->title;
}
print $form->selectarray('expression_selection', $price_expression_list, $eid);
print '</td></tr>';
// Title input
print '<tr><td class="fieldrequired">'.$langs->trans("Name").'</td><td>';
print '<input class="flat" name="expression_title" size="15" value="'.($price_expression->title?$price_expression->title:'').'">';
print '</td></tr>';
//Price expression editor
print '<tr><td class="fieldrequired">'.$form->textwithpicto($langs->trans("PriceExpressionEditor"),$langs->trans("PriceExpressionEditorHelp"),1).'</td><td>';
require_once DOL_DOCUMENT_ROOT.'/core/class/doleditor.class.php';
$doleditor=new DolEditor('expression',isset($price_expression->expression)?$price_expression->expression:'','',300,'','',false,false,false,4,80);
$doleditor->Create();
print '</td></tr>';
print '</table>';
//Buttons
print '<center>';
print '<input type="submit" class="butAction" value="'.$langs->trans("Save").'">';
print '<span id="back" class="butAction">'.$langs->trans("Back").'</span>';
if ($eid == 0)
{
print '<div class="inline-block divButAction"><span id="action-delete" class="butActionRefused">'.$langs->trans('Delete').'</span></div>'."\n";
}
else
{
print '<div class="inline-block divButAction"><a class="butActionDelete" href="'.$_SERVER["PHP_SELF"].'?id='.$id.'&amp;tab='.$tab.'&amp;eid='.$eid.'&amp;action=delete">'.$langs->trans("Delete").'</a></div>';
}
print '</center>';
print '</form>';
// This code reloads the page depending of selected option, goes back in history when back is pressed
print '<script type="text/javascript">
jQuery(document).ready(run);
function run() {
jQuery("#back").click(on_click);
jQuery("#expression_selection").change(on_change);
}
function on_click() {
window.location = "'.str_replace('expression.php', $tab.'.php', $_SERVER["PHP_SELF"]).'?id='.$id.'";
}
function on_change() {
window.location = "'.$_SERVER["PHP_SELF"].'?id='.$id.'&tab='.$tab.'&eid=" + $("#expression_selection").attr("value");
}
</script>';
llxFooter();
$db->close();

71
htdocs/product/fournisseurs.php Normal file → Executable file
View File

@ -5,6 +5,7 @@
* Copyright (C) 2005-2012 Regis Houssin <regis.houssin@capnetworks.com>
* Copyright (C) 2010-2012 Juanjo Menent <jmenent@2byte.es>
* Copyright (C) 2012 Christophe Battarel <christophe.battarel@altairis.fr>
* Copyright (C) 2014 Ion Agorria <ion@agorria.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -30,6 +31,8 @@ require '../main.inc.php';
require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php';
require_once DOL_DOCUMENT_ROOT.'/comm/propal/class/propal.class.php';
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
require_once DOL_DOCUMENT_ROOT.'/product/class/priceexpression.class.php';
require_once DOL_DOCUMENT_ROOT.'/product/class/priceparser.class.php';
$langs->load("products");
$langs->load("suppliers");
@ -103,6 +106,7 @@ if ($action == 'updateprice' && GETPOST('cancel') <> $langs->trans("Cancel"))
$npr = preg_match('/\*/', $_POST['tva_tx']) ? 1 : 0 ;
$tva_tx = str_replace('*','', GETPOST('tva_tx','alpha'));
$tva_tx = price2num($tva_tx);
$price_expression = GETPOST('eid', 'int') == 0 ? 'NULL' : GETPOST('eid', 'int'); //Discard expression if not in expression mode
if ($tva_tx == '')
{
@ -126,8 +130,14 @@ if ($action == 'updateprice' && GETPOST('cancel') <> $langs->trans("Cancel"))
}
if ($_POST["price"] < 0 || $_POST["price"] == '')
{
$error++;
setEventMessage($langs->trans("ErrorFieldRequired",$langs->transnoentities("Price")), 'errors');
if ($price_expression == 'NULL') { //This is not because of using expression instead of numeric price
$error++;
setEventMessage($langs->trans("ErrorFieldRequired",$langs->transnoentities("Price")), 'errors');
}
else
{
$_POST["price"] = 0;
}
}
$product = new ProductFournisseur($db);
@ -173,6 +183,26 @@ if ($action == 'updateprice' && GETPOST('cancel') <> $langs->trans("Cancel"))
{
$error++;
setEventMessage($product->error, 'errors');
}
else
{
if ($price_expression != 'NULL') {
//Check the expression validity by parsing it
$priceparser = new PriceParser($db);
$price_result = $priceparser->parse_product_supplier($id, $price_expression, $quantity, $tva_tx);
if ($price_result < 0) { //Expression is not valid
$error++;
setEventMessage($priceparser->translated_error(), 'errors');
}
}
if (! $error && ! empty($conf->dynamicprices->enabled)) {
$ret=$product->set_price_expression($price_expression);
if ($ret < 0)
{
$error++;
setEventMessage($product->error, 'errors');
}
}
}
}
@ -266,7 +296,7 @@ if ($id || $ref)
if ($rowid)
{
$product->fetch_product_fournisseur_price($rowid);
$product->fetch_product_fournisseur_price($rowid, 1); //Ignore the math expression when getting the price
print_fiche_titre($langs->trans("ChangeSupplierPrice"));
}
else
@ -368,8 +398,41 @@ if ($id || $ref)
print '<input type="text" class="flat" size="5" name="tva_tx" value="'.(GETPOST("tva_tx")?vatrate(GETPOST("tva_tx")):($default_vat!=''?vatrate($default_vat):'')).'">';
print '</td></tr>';
if (! empty($conf->dynamicprices->enabled)) { //Only show price mode and expression selector if module is enabled
// Price mode selector
print '<tr><td class="fieldrequired">'.$langs->trans("PriceMode").'</td><td>';
$price_expression = new PriceExpression($db);
$price_expression_list = array(0 => $langs->trans("PriceNumeric")); //Put the numeric mode as first option
foreach ($price_expression->list_price_expression() as $entry) {
$price_expression_list[$entry->id] = $entry->title;
}
$price_expression_preselection = GETPOST('eid') ? GETPOST('eid') : ($product->fk_price_expression ? $product->fk_price_expression : '0');
print $form->selectarray('eid', $price_expression_list, $price_expression_preselection);
print '&nbsp; <div id="expression_editor" class="button">'.$langs->trans("PriceExpressionEditor").'</div>';
print '</td></tr>';
// This code hides the numeric price input if is not selected, loads the editor page if editor button is pressed
print '<script type="text/javascript">
jQuery(document).ready(run);
function run() {
jQuery("#expression_editor").click(on_click);
jQuery("#eid").change(on_change);
on_change();
}
function on_click() {
window.location = "'.DOL_URL_ROOT.'/product/expression.php?id='.$id.'&tab=fournisseurs&eid=" + $("#eid").attr("value");
}
function on_change() {
if ($("#eid").attr("value") == 0) {
jQuery("#price_numeric").show();
} else {
jQuery("#price_numeric").hide();
}
}
</script>';
}
// Price qty min
print '<tr><td class="fieldrequired">'.$langs->trans("PriceQtyMin").'</td>';
print '<tr id="price_numeric"><td class="fieldrequired">'.$langs->trans("PriceQtyMin").'</td>';
print '<td><input class="flat" name="price" size="8" value="'.(GETPOST('price')?price(GETPOST('price')):(isset($product->fourn_price)?price($product->fourn_price):'')).'">';
print '&nbsp;';
print $form->select_PriceBaseType((GETPOST('price_base_type')?GETPOST('price_base_type'):$product->price_base_type), "price_base_type");