diff --git a/htdocs/attributes/admin/admin.php b/htdocs/attributes/admin/admin.php new file mode 100644 index 00000000000..eedacb086b3 --- /dev/null +++ b/htdocs/attributes/admin/admin.php @@ -0,0 +1,65 @@ + + * + * 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 . + */ + +require '../../main.inc.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/admin.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php'; + +$langs->load("admin"); +$langs->load("products"); + +// Security check +if (! $user->admin || (empty($conf->product->enabled) && empty($conf->service->enabled))) + accessforbidden(); + +if ($_POST) { + + $value = GETPOST('PRODUIT_ATTRIBUTES_HIDECHILD'); + + if (dolibarr_set_const($db, 'PRODUIT_ATTRIBUTES_HIDECHILD', $value, 'chaine', 0, '', $conf->entity)) { + setEventMessage($langs->trans('RecordSaved')); + } else { + setEventMessage($langs->trans('CoreErrorMessage'), 'errors'); + } + +} + +$title = $langs->trans('ModuleSetup').' '.$langs->trans('ProductAttributes'); +llxHeader('', $title); + +$linkback=''.$langs->trans("BackToModuleList").''; +print load_fiche_titre($title,$linkback,'title_setup'); + +dol_fiche_head(array(), 'general', $tab, 0, 'product'); + +print '
'; +print ''; +print ''; +print ''."\n"; +print ''."\n"; +print ''."\n"; +print ''; +print '
'.$langs->trans("Parameters").''.$langs->trans("Value").' 
'.$langs->trans('HideProductCombinations').''; +print $form->selectyesno("PRODUIT_ATTRIBUTES_HIDECHILD",$conf->global->PRODUIT_ATTRIBUTES_HIDECHILD,1).'
'; +print '
'; +print '
'; + +llxFooter(); + +$db->close(); + diff --git a/htdocs/attributes/admin/index.html b/htdocs/attributes/admin/index.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/htdocs/attributes/ajax/getCombinations.php b/htdocs/attributes/ajax/getCombinations.php new file mode 100644 index 00000000000..5ed3e959ea3 --- /dev/null +++ b/htdocs/attributes/ajax/getCombinations.php @@ -0,0 +1,50 @@ + + * + * 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 . + */ + +define('NOTOKENRENEWAL','1'); +define('NOREQUIREMENU','1'); +define('NOREQUIREHTML','1'); +define('NOREQUIREAJAX','1'); +define('NOREQUIRESOC','1'); + +require '../../main.inc.php'; +require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; +require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductCombination.class.php'; + +header('Content-Type: application/json'); + +$id = GETPOST('id'); + +if (!$id) { + print json_encode(array( + 'error' => 'ID not set' + )); + die; +} + +$product = new Product($db); + +if ($product->fetch($id) < 0) { + print json_encode(array( + 'error' => 'Product not found' + )); +} + +$prodcomb = new ProductCombination($db); + +echo json_encode($prodcomb->getUniqueAttributesAndValuesByFkProductParent($product->id)); diff --git a/htdocs/attributes/ajax/get_attribute_values.php b/htdocs/attributes/ajax/get_attribute_values.php new file mode 100644 index 00000000000..a08126d997a --- /dev/null +++ b/htdocs/attributes/ajax/get_attribute_values.php @@ -0,0 +1,61 @@ + + * + * 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 . + */ + +define('NOTOKENRENEWAL','1'); +define('NOREQUIREMENU','1'); +define('NOREQUIREHTML','1'); +define('NOREQUIREAJAX','1'); +define('NOREQUIRESOC','1'); + +require '../../main.inc.php'; +require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; +require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductAttribute.class.php'; +require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductAttributeValue.class.php'; + +header('Content-Type: application/json'); + +$id = GETPOST('id'); + +if (!$id) { + print json_encode(array( + 'error' => 'ID not set' + )); + die; +} + +$prodattr = new ProductAttribute($db); + +if ($prodattr->fetch($id) < 0) { + print json_encode(array( + 'error' => 'Attribute not found' + )); + die; +} + +$prodattrval = new ProductAttributeValue($db); + +$res = $prodattrval->fetchAllByProductAttribute($id); + +if ($res == -1) { + print json_encode(array( + 'error' => 'Internal error' + )); + die; +} + +print json_encode($res); \ No newline at end of file diff --git a/htdocs/attributes/ajax/index.html b/htdocs/attributes/ajax/index.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/htdocs/attributes/ajax/orderAttribute.php b/htdocs/attributes/ajax/orderAttribute.php new file mode 100644 index 00000000000..6c7eecc9453 --- /dev/null +++ b/htdocs/attributes/ajax/orderAttribute.php @@ -0,0 +1,53 @@ + + * + * 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 . + */ + +if (! defined('NOTOKENRENEWAL')) define('NOTOKENRENEWAL','1'); // Disable token renewal +if (! defined('NOREQUIREMENU')) define('NOREQUIREMENU','1'); +if (! defined('NOREQUIREHTML')) define('NOREQUIREHTML','1'); +if (! defined('NOREQUIREAJAX')) define('NOREQUIREAJAX','1'); +if (! defined('NOREQUIRESOC')) define('NOREQUIRESOC','1'); +if (! defined('NOREQUIRETRAN')) define('NOREQUIRETRAN','1'); +if (! defined('NOREQUIREHOOK')) define('NOREQUIREHOOK','1'); // Disable "main.inc.php" hooks + +require '../../main.inc.php'; + +/* + * View + */ + +top_httphead(); + +// Registering the location of boxes +if (isset($_POST['roworder'])) { + $roworder=GETPOST('roworder','alpha',2); + + dol_syslog("AjaxOrderAttribute roworder=".$roworder, LOG_DEBUG); + + $rowordertab = explode(',', $roworder); + + foreach ($rowordertab as $value) { + if (!empty($value)) { + $newrowordertab[] = $value; + } + } + + require DOL_DOCUMENT_ROOT.'/attributes/class/ProductAttribute.class.php'; + + ProductAttribute::bulkUpdateOrder($db, $newrowordertab); +} + diff --git a/htdocs/attributes/card.php b/htdocs/attributes/card.php new file mode 100644 index 00000000000..3339a169c89 --- /dev/null +++ b/htdocs/attributes/card.php @@ -0,0 +1,245 @@ + + * + * 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 . + */ + +require '../main.inc.php'; +require 'class/ProductAttribute.class.php'; +require 'class/ProductAttributeValue.class.php'; + +$id = GETPOST('id'); +$valueid = GETPOST('valueid'); +$action = GETPOST('action'); +$label = GETPOST('label'); +$ref = GETPOST('ref'); +$confirm = GETPOST('confirm'); + +$prodattr = new ProductAttribute($db); +$prodattrval = new ProductAttributeValue($db); + +if ($prodattr->fetch($id) < 1) { + dol_print_error($db, $langs->trans('ErrorRecordNotFound')); + die; +} + +if ($_POST) { + + if ($action == 'edit') { + + $prodattr->label = $label; + $prodattr->ref = $ref; + + if ($prodattr->update() < 1) { + setEventMessage($langs->trans('CoreErrorMessage'), 'errors'); + } else { + setEventMessage($langs->trans('RecordSaved')); + header('Location: '.dol_buildpath('/attributes/card.php?id='.$id, 2)); + die; + } + } elseif ($action == 'edit_value') { + + if ($prodattrval->fetch($valueid) > 0) { + + $prodattrval->ref = $ref; + $prodattrval->value = GETPOST('value'); + + if ($prodattrval->update() > 0) { + setEventMessage($langs->trans('RecordSaved')); + } else { + setEventMessage($langs->trans('CoreErrorMessage'), 'errors'); + } + } + + header('Location: '.dol_buildpath('/attributes/card.php?id='.$prodattr->id, 2)); + die; + } + +} + +if ($confirm == 'yes') { + if ($action == 'confirm_delete') { + + $db->begin(); + + $res = $prodattrval->deleteByFkAttribute($prodattr->id); + + if ($res < 1 || ($prodattr->delete() < 1)) { + $db->rollback(); + setEventMessage($langs->trans('CoreErrorMessage'), 'errors'); + header('Location: '.dol_buildpath('/attributes/card.php?id='.$prodattr->id, 2)); + } else { + $db->commit(); + setEventMessage($langs->trans('RecordSaved')); + header('Location: '.dol_buildpath('/attributes/list.php', 2)); + } + + die; + } elseif ($action == 'confirm_deletevalue') { + + if ($prodattrval->fetch($valueid) > 0) { + + if ($prodattrval->delete() < 1) { + setEventMessage($langs->trans('CoreErrorMessage'), 'errors'); + } else { + setEventMessage($langs->trans('RecordSaved')); + } + + header('Location: '.dol_buildpath('/attributes/card.php?id='.$prodattr->id, 2)); + die; + } + } +} + +$langs->load('products'); + +$title = $langs->trans('ProductAttributeName', dol_htmlentities($prodattr->label)); +$var = false; + +llxHeader('', $title); + +print_fiche_titre($title); + +dol_fiche_head(); + +if ($action == 'edit') { + print '
'; +} + +?> + + + + + + + + + + +
trans('Ref') ?> + ref.'">'; + } else { + print dol_htmlentities($prodattr->ref); + } ?> +
trans('Label') ?> + label.'">'; + } else { + print dol_htmlentities($prodattr->label); + } ?> +
+ + +
+ +
+formconfirm( + "card.php?id=".$prodattr->id, + $langs->trans('Delete'), + $langs->trans('ProductAttributeDeleteDialog'), + "confirm_delete", + '', + 0, + 1 + ); + } elseif ($action == 'delete_value') { + + if ($prodattrval->fetch($valueid) > 0) { + + $form = new Form($db); + + print $form->formconfirm( + "card.php?id=".$prodattr->id."&valueid=".$prodattrval->id, + $langs->trans('Delete'), + $langs->trans('ProductAttributeValueDeleteDialog', dol_htmlentities($prodattrval->value), dol_htmlentities($prodattrval->ref)), + "confirm_deletevalue", + '', + 0, + 1 + ); + } + } + + ?> + + + + +
+ + + + + + + + + + fetchAllByProductAttribute($prodattr->id) as $attrval): ?> + > + id)): ?> + + + + + + + + + + +
trans('Ref') ?>trans('Value') ?>
+ + trans('Cancel') ?> + ref) ?>value) ?> + + +
+ + +
+ + + + + + * + * 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 . + */ + +/** + * Class ProductAttribute + * Used to represent a product attribute + */ +class ProductAttribute +{ + /** + * Database handler + * @var DoliDB + */ + private $db; + + /** + * Id of the product attribute + * @var int + */ + public $id; + + /** + * Ref of the product attribute + * @var + */ + public $ref; + + /** + * Label of the product attribute + * @var string + */ + public $label; + + /** + * Order of attribute. + * Lower ones will be shown first and higher ones last + * @var int + */ + public $rang; + + public function __construct(DoliDB $db) + { + global $conf; + + $this->db = $db; + $this->entity = $conf->entity; + } + + /** + * Fetches the properties of a product attribute + * + * @param int $id Attribute id + * @return int <1 KO, >1 OK + */ + public function fetch($id) + { + if (!$id) { + return -1; + } + + require_once __DIR__.'/../lib/product_attributes.lib.php'; + + $sql = "SELECT rowid, ref, label, rang FROM ".MAIN_DB_PREFIX."product_attribute WHERE rowid = ".(int) $id." AND entity IN (".getProductEntities($this->db).")"; + + $query = $this->db->query($sql); + + if (!$this->db->num_rows($query)) { + return -1; + } + + $result = $this->db->fetch_object($query); + + $this->id = $result->rowid; + $this->ref = $result->ref; + $this->label = $result->label; + $this->rang = $result->rang; + + return 1; + } + + /** + * Returns an array of all product attributes + * + * @return ProductAttribute[] + */ + public function fetchAll() + { + require_once __DIR__.'/../lib/product_attributes.lib.php'; + + $return = array(); + + $sql = 'SELECT rowid, ref, label, rang FROM '.MAIN_DB_PREFIX."product_attribute WHERE entity IN (".getProductEntities($this->db).')'; + $sql .= $this->db->order('rang', 'asc'); + $query = $this->db->query($sql); + + while ($result = $this->db->fetch_object($query)) { + + $tmp = new ProductAttribute($this->db); + $tmp->id = $result->rowid; + $tmp->ref = $result->ref; + $tmp->label = $result->label; + $tmp->rang = $result->rang; + + $return[] = $tmp; + } + + return $return; + } + + /** + * Creates a product attribute + * + * @return int <0 KO, >0 OK + */ + public function create() + { + //Ref must be uppercase + $this->ref = strtoupper($this->ref); + + $sql = "INSERT INTO ".MAIN_DB_PREFIX."product_attribute (ref, label, entity, rang) + VALUES ('".$this->db->escape($this->ref)."', '".$this->db->escape($this->label)."', ".(int) $this->entity.", ".(int) $this->rang.")"; + $query = $this->db->query($sql); + + if ($query) { + $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.'product_attribute'); + + return 1; + } + + return -1; + } + + /** + * Updates a product attribute + * + * @return int <0 KO, >0 OK + */ + public function update() + { + //Ref must be uppercase + $this->ref = strtoupper($this->ref); + + $sql = "UPDATE ".MAIN_DB_PREFIX."product_attribute SET ref = '".$this->db->escape($this->ref)."', label = '".$this->db->escape($this->label)."', rang = ".(int) $this->rang." WHERE rowid = ".(int) $this->id; + + if ($this->db->query($sql)) { + return 1; + } + + return -1; + } + + /** + * Deletes a product attribute + * + * @return int <0 KO, >0 OK + */ + public function delete() + { + $sql = "DELETE FROM ".MAIN_DB_PREFIX."product_attribute WHERE rowid = ".(int) $this->id; + + if ($this->db->query($sql)) { + return 1; + } + + return -1; + } + + /** + * Returns the number of products that are using this attribute + * + * @return int + */ + public function countChildProducts() + { + require_once __DIR__.'/../lib/product_attributes.lib.php'; + + $sql = "SELECT COUNT(*) count FROM ".MAIN_DB_PREFIX."product_attribute_combination2val pac2v + LEFT JOIN ".MAIN_DB_PREFIX."product_attribute_combination pac ON pac2v.fk_prod_combination = pac.rowid WHERE pac2v.fk_prod_attr = ".(int) $this->id." AND pac.entity IN (".getProductEntities($this->db).")"; + + $query = $this->db->query($sql); + + $result = $this->db->fetch_object($query); + + return $result->count; + } + + /** + * Reorders the order of the attributes. + * This is an internal function used by moveLine function + * + * @return int <0 KO >0 OK + */ + protected function reorderLines() + { + $tmp_order = array(); + + $sql = 'SELECT rowid FROM '.MAIN_DB_PREFIX.'product_attribute WHERE rang = 0'; + $sql .= $this->db->order('rang, rowid', 'asc'); + + $query = $this->db->query($sql); + + if (!$query) { + return -1; + } + + while ($result = $this->db->fetch_object($query)) { + $tmp_order[] = $result->rowid; + } + + foreach ($tmp_order as $order => $rowid) { + $tmp = new ProductAttribute($this->db); + $tmp->fetch($rowid); + $tmp->rang = $order+1; + + if ($tmp->update() < 0) { + return -1; + } + } + + return 1; + } + + /** + * Internal function to handle moveUp and moveDown functions + * + * @param string $type up/down + * @return int <0 KO >0 OK + */ + private function moveLine($type) + { + if ($this->reorderLines() < 0) { + return -1; + } + + $this->db->begin(); + + if ($type == 'up') { + $newrang = $this->rang - 1; + } else { + $newrang = $this->rang + 1; + } + + $sql = 'UPDATE '.MAIN_DB_PREFIX.'product_attribute SET rang = '.$this->rang.' WHERE rang = '.$newrang; + + if (!$this->db->query($sql)) { + $this->db->rollback(); + return -1; + } + + $this->rang = $newrang; + + if ($this->update() < 0) { + $this->db->rollback(); + return -1; + } + + $this->db->commit(); + return 1; + } + + /** + * Shows this attribute before others + * + * @return int <0 KO >0 OK + */ + public function moveUp() + { + return $this->moveLine('up'); + } + + /** + * Shows this attribute after others + * + * @return int <0 KO >0 OK + */ + public function moveDown() + { + return $this->moveLine('down'); + } + + /** + * Updates the order of all attributes. Used by AJAX page for drag&drop + * + * @param DoliDB $db Database handler + * @param array $order Array with row id ordered in ascendent mode + * @return int <0 KO >0 OK + */ + public static function bulkUpdateOrder(DoliDB $db, array $order) + { + $tmp = new ProductAttribute($db); + + foreach ($order as $key => $attrid) { + if ($tmp->fetch($attrid) < 0) { + return -1; + } + + $tmp->rang = $key; + + if ($tmp->update() < 0) { + return -1; + } + } + + return 1; + } +} \ No newline at end of file diff --git a/htdocs/attributes/class/ProductAttributeValue.class.php b/htdocs/attributes/class/ProductAttributeValue.class.php new file mode 100644 index 00000000000..41d1818ee63 --- /dev/null +++ b/htdocs/attributes/class/ProductAttributeValue.class.php @@ -0,0 +1,225 @@ + + * + * 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 . + */ + +/** + * Class ProductAttributeValue + * Used to represent a product attribute value + */ +class ProductAttributeValue +{ + /** + * Database handler + * @var DoliDB + */ + private $db; + + /** + * Attribute value id + * @var int + */ + public $id; + + /** + * Product attribute id + * @var int + */ + public $fk_product_attribute; + + /** + * Attribute value ref + * @var string + */ + public $ref; + + /** + * Attribute value value + * @var string + */ + public $value; + + public function __construct(DoliDB $db) + { + global $conf; + + $this->db = $db; + $this->entity = $conf->entity; + } + + /** + * Gets a product attribute value + * + * @param int $valueid Product attribute value id + * @return int <0 KO, >0 OK + */ + public function fetch($valueid) + { + require_once __DIR__.'/../lib/product_attributes.lib.php'; + + $sql = "SELECT rowid, fk_product_attribute, ref, value FROM ".MAIN_DB_PREFIX."product_attribute_value WHERE rowid = ".(int) $valueid." AND entity IN (".getProductEntities($this->db).")"; + + $query = $this->db->query($sql); + + if (!$query) { + return -1; + } + + if (!$this->db->num_rows($query)) { + return -1; + } + + $result = $this->db->fetch_object($query); + + $this->id = $result->rowid; + $this->fk_product_attribute = $result->fk_product_attribute; + $this->ref = $result->ref; + $this->value = $result->value; + + return 1; + } + + /** + * Returns all product attribute values of a product attribute + * + * @param int $prodattr_id Product attribute id + * @param bool $only_used Fetch only used attribute values + * @return ProductAttributeValue[] + */ + public function fetchAllByProductAttribute($prodattr_id, $only_used = false) + { + require_once __DIR__.'/../lib/product_attributes.lib.php'; + + $return = array(); + + $sql = 'SELECT '; + + if ($only_used) { + $sql .= 'DISTINCT '; + } + + $sql .= 'v.fk_product_attribute, v.rowid, v.ref, v.value FROM '.MAIN_DB_PREFIX.'product_attribute_value v '; + + if ($only_used) { + $sql .= 'LEFT JOIN '.MAIN_DB_PREFIX.'product_attribute_combination2val c2v ON c2v.fk_prod_attr_val = v.rowid '; + $sql .= 'LEFT JOIN '.MAIN_DB_PREFIX.'product_attribute_combination c ON c.rowid = c2v.fk_prod_combination '; + $sql .= 'LEFT JOIN '.MAIN_DB_PREFIX.'product p ON p.rowid = c.fk_product_child '; + } + + $sql .= 'WHERE v.fk_product_attribute = '.(int) $prodattr_id; + + if ($only_used) { + $sql .= ' AND c2v.rowid IS NOT NULL AND p.tosell = 1'; + } + + $query = $this->db->query($sql); + + while ($result = $this->db->fetch_object($query)) { + + $tmp = new ProductAttributeValue($this->db); + $tmp->fk_product_attribute = $result->fk_product_attribute; + $tmp->id = $result->rowid; + $tmp->ref = $result->ref; + $tmp->value = $result->value; + + $return[] = $tmp; + } + + return $return; + } + + /** + * Creates a value for a product attribute + * + * @return int <0 KO >0 OK + */ + public function create() + { + if (!$this->fk_product_attribute) { + return -1; + } + + //Ref must be uppercase + $this->ref = strtoupper($this->ref); + + $sql = "INSERT INTO ".MAIN_DB_PREFIX."product_attribute_value (fk_product_attribute, ref, value, entity) + VALUES ('".(int) $this->fk_product_attribute."', '".$this->db->escape($this->ref)."', + '".$this->db->escape($this->value)."', ".(int) $this->entity.")"; + + $query = $this->db->query($sql); + + if ($query) { + $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.'product_attribute_value'); + return 1; + } + + return -1; + } + + /** + * Updates a product attribute value + * + * @return int + */ + public function update() + { + //Ref must be uppercase + $this->ref = strtoupper($this->ref); + + $sql = "UPDATE ".MAIN_DB_PREFIX."product_attribute_value + SET fk_product_attribute = '".(int) $this->fk_product_attribute."', ref = '".$this->db->escape($this->ref)."', + value = '".$this->db->escape($this->value)."' WHERE rowid = ".(int) $this->id; + + if ($this->db->query($sql)) { + return 1; + } + + return -1; + } + + /** + * Deletes a product attribute value + * + * @return int <0 KO, >0 OK + */ + public function delete() + { + $sql = "DELETE FROM ".MAIN_DB_PREFIX."product_attribute_value WHERE rowid = ".(int) $this->id; + + if ($this->db->query($sql)) { + return 1; + } + + return -1; + } + + /** + * Deletes all product attribute values by a product attribute id + * + * @param int $fk_attribute Product attribute id + * @return int <0 KO, >0 OK + */ + public function deleteByFkAttribute($fk_attribute) + { + $sql = "DELETE FROM ".MAIN_DB_PREFIX."product_attribute_value WHERE fk_product_attribute = ".(int) $fk_attribute; + + if ($this->db->query($sql)) { + return 1; + } + + return -1; + } +} \ No newline at end of file diff --git a/htdocs/attributes/class/ProductCombination.class.php b/htdocs/attributes/class/ProductCombination.class.php new file mode 100644 index 00000000000..d5d7b6e81af --- /dev/null +++ b/htdocs/attributes/class/ProductCombination.class.php @@ -0,0 +1,645 @@ + + * + * 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 . + */ + +/** + * Class ProductCombination + * Used to represent a product combination + */ +class ProductCombination +{ + /** + * Database handler + * @var DoliDB + */ + private $db; + + /** + * Rowid of combination + * @var int + */ + public $id; + + /** + * Rowid of parent product + * @var int + */ + public $fk_product_parent; + + /** + * Rowid of child product + * @var int + */ + public $fk_product_child; + + /** + * Price variation + * @var float + */ + public $variation_price; + + /** + * Is the price variation a relative variation? + * @var bool + */ + public $variation_price_percentage = false; + + /** + * Weight variation + * @var float + */ + public $variation_weight; + + /** + * Combination entity + * @var int + */ + public $entity; + + public function __construct(DoliDB $db) + { + global $conf; + + $this->db = $db; + $this->entity = $conf->entity; + } + + /** + * Retrieves a combination by its rowid + * + * @param int $rowid Row id + * @return int <0 KO, >0 OK + */ + public function fetch($rowid) + { + require_once __DIR__.'/../lib/product_attributes.lib.php'; + + $sql = "SELECT rowid, fk_product_parent, fk_product_child, variation_price, variation_price_percentage, variation_weight FROM ".MAIN_DB_PREFIX."product_attribute_combination WHERE rowid = ".(int) $rowid." AND entity IN (".getProductEntities($this->db).")"; + + $query = $this->db->query($sql); + + if (!$query) { + return -1; + } + + if (!$this->db->num_rows($query)) { + return -1; + } + + $result = $this->db->fetch_object($query); + + $this->id = $result->rowid; + $this->fk_product_parent = $result->fk_product_parent; + $this->fk_product_child = $result->fk_product_child; + $this->variation_price = $result->variation_price; + $this->variation_price_percentage = $result->variation_price_percentage; + $this->variation_weight = $result->variation_weight; + + return 1; + } + + /** + * Retrieves a product combination by a child product row id + * + * @param int $fk_child Product row id + * @return int <0 KO, >0 OK + */ + public function fetchByFkProductChild($fk_child) + { + require_once __DIR__.'/../lib/product_attributes.lib.php'; + + $sql = "SELECT rowid, fk_product_parent, fk_product_child, variation_price, variation_price_percentage, variation_weight FROM ".MAIN_DB_PREFIX."product_attribute_combination WHERE fk_product_child = ".(int) $fk_child." AND entity IN (".getProductEntities($this->db).")"; + + $query = $this->db->query($sql); + + if (!$query) { + return -1; + } + + if (!$this->db->num_rows($query)) { + return -1; + } + + $result = $this->db->fetch_object($query); + + $this->id = $result->rowid; + $this->fk_product_parent = $result->fk_product_parent; + $this->fk_product_child = $result->fk_product_child; + $this->variation_price = $result->variation_price; + $this->variation_price_percentage = $result->variation_price_percentage; + $this->variation_weight = $result->variation_weight; + + return 1; + } + + /** + * Retrieves all product combinations by the product parent row id + * + * @param int $fk_product_parent Rowid of parent product + * @return int|ProductCombination[] <0 KO + */ + public function fetchAllByFkProductParent($fk_product_parent) + { + require_once __DIR__.'/../lib/product_attributes.lib.php'; + + $sql = "SELECT rowid, fk_product_parent, fk_product_child, variation_price, variation_price_percentage, variation_weight FROM ".MAIN_DB_PREFIX."product_attribute_combination WHERE fk_product_parent = ".(int) $fk_product_parent." AND entity IN (".getProductEntities($this->db).")"; + + $query = $this->db->query($sql); + + if (!$query) { + return -1; + } + + $return = array(); + + while ($result = $this->db->fetch_object($query)) { + + $tmp = new ProductCombination($this->db); + $tmp->id = $result->rowid; + $tmp->fk_product_parent = $result->fk_product_parent; + $tmp->fk_product_child = $result->fk_product_child; + $tmp->variation_price = $result->variation_price; + $tmp->variation_price_percentage = $result->variation_price_percentage; + $tmp->variation_weight = $result->variation_weight; + + $return[] = $tmp; + } + + return $return; + } + + /** + * Creates a product attribute combination + * + * @return int + */ + public function create() + { + $sql = "INSERT INTO ".MAIN_DB_PREFIX."product_attribute_combination + (fk_product_parent, fk_product_child, variation_price, variation_price_percentage, variation_weight, entity) + VALUES (".(int) $this->fk_product_parent.", ".(int) $this->fk_product_child.", + ".(float) $this->variation_price.", ".(int) $this->variation_price_percentage.", + ".(float) $this->variation_weight.", ".(int) $this->entity.")"; + + $query = $this->db->query($sql); + + if (!$query) { + return -1; + } + + $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.'product_attribute_combination'); + + return 1; + } + + /** + * Updates a product combination + * + * @return int <0 KO, >0 OK + */ + public function update() + { + $sql = "UPDATE ".MAIN_DB_PREFIX."product_attribute_combination + SET fk_product_parent = ".(int) $this->fk_product_parent.", fk_product_child = ".(int) $this->fk_product_child.", + variation_price = ".(float) $this->variation_price.", variation_price_percentage = ".(int) $this->variation_price_percentage.", + variation_weight = ".(float) $this->variation_weight." WHERE rowid = ".(int) $this->id; + + $query = $this->db->query($sql); + + if (!$query) { + return -1; + } + + $parent = new Product($this->db); + $parent->fetch($this->fk_product_parent); + + $this->updateProperties($parent); + + return 1; + } + + /** + * Deletes a product combination + * + * @return int <0 KO >0 OK + */ + public function delete() + { + $this->db->begin(); + + $comb2val = new ProductCombination2ValuePair($this->db); + $comb2val->deleteByFkCombination($this->id); + + $sql = "DELETE FROM ".MAIN_DB_PREFIX."product_attribute_combination WHERE rowid = ".(int) $this->id; + + if ($this->db->query($sql)) { + $this->db->commit(); + return 1; + } + + $this->db->rollback(); + return -1; + } + + /** + * Deletes all product combinations of a parent product + * + * @param int $fk_product_parent Rowid of parent product + * @return int <0 KO >0 OK + */ + public function deleteByFkProductParent($fk_product_parent) + { + $this->db->begin(); + + foreach ($this->fetchAllByFkProductParent($fk_product_parent) as $prodcomb) { + + $prodstatic = new Product($this->db); + + $res = $prodstatic->fetch($prodcomb->fk_product_child); + + if ($res > 0) { + $res = $prodcomb->delete(); + } + + if ($res > 0 && !$prodstatic->isObjectUsed($prodstatic->id)) { + $res = $prodstatic->delete(); + } + + if ($res < 0) { + $this->db->rollback(); + return -1; + } + } + + $this->db->commit(); + return 1; + } + + /** + * Updates the weight of the child product. The price must be updated using Product::updatePrices + * + * @param Product $parent Parent product + * @return int >0 OK <0 KO + */ + public function updateProperties(Product $parent) + { + global $user, $conf; + + $this->db->begin(); + + $child = new Product($this->db); + $child->fetch($this->fk_product_child); + $child->price_autogen = $parent->price_autogen; + $child->weight = $parent->weight + $this->variation_weight; + $child->weight_units = $parent->weight_units; + + if ($child->update($child->id, $user) > 0) { + + $new_vat = $parent->tva_tx; + $new_npr = $parent->tva_npr; + + // MultiPrix + if (! empty($conf->global->PRODUIT_MULTIPRICES)) { + $new_type = $parent->multiprices_base_type[1]; + $new_min_price = $parent->multiprices_min[1]; + $new_psq = $parent->multiprices_recuperableonly[1]; + + if ($new_type == 'TTC') { + $new_price = $parent->multiprices_ttc[1]; + } else { + $new_price = $parent->multiprices[1]; + } + } else { + $new_type = $parent->price_base_type; + $new_min_price = $parent->price_min; + $new_psq = $parent->price_by_qty; + + if ($new_type == 'TTC') { + $new_price = $parent->price_ttc; + } else { + $new_price = $parent->price; + } + } + + if ($this->variation_price_percentage) { + $new_price *= 1 + ($this->variation_price/100); + } else { + $new_price += $this->variation_price; + } + + $child->updatePrice($new_price, $new_type, $user, $new_vat, $new_min_price, 1, $new_npr, $new_psq); + + $this->db->commit(); + + return 1; + } + + $this->db->rollback(); + return -1; + } + + /** + * Retrieves the combination that matches the given features. + * + * @param int $prodid Id of parent product + * @param array $features Format: [$attr] => $attr_val + * @return false|ProductCombination false if not found + */ + public function fetchByProductCombination2ValuePairs($prodid, array $features) + { + require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductCombination2ValuePair.class.php'; + + $actual_comp = array(); + + $prodcomb2val = new ProductCombination2ValuePair($this->db); + $prodcomb = new ProductCombination($this->db); + + foreach ($features as $attr => $attr_val) { + $actual_comp[$attr] = $attr_val; + } + + foreach ($prodcomb->fetchAllByFkProductParent($prodid) as $prc) { + + $values = array(); + + foreach ($prodcomb2val->fetchByFkCombination($prc->id) as $value) { + $values[$value->fk_prod_attr] = $value->fk_prod_attr_val; + } + + $check1 = count(array_diff_assoc($values, $actual_comp)); + $check2 = count(array_diff_assoc($actual_comp, $values)); + + if (!$check1 && !$check2) { + return $prc; + } + } + + return false; + } + + /** + * Retrieves all unique attributres for a parent product + * + * @param int $productid Product rowid + * @return ProductAttribute[] + */ + public function getUniqueAttributesAndValuesByFkProductParent($productid) + { + require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductAttribute.class.php'; + require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductAttributeValue.class.php'; + + $attributes = array(); + + //Attributes + $sql = "SELECT DISTINCT fk_prod_attr, a.rang +FROM ".MAIN_DB_PREFIX."product_attribute_combination2val c2v LEFT JOIN ".MAIN_DB_PREFIX."product_attribute_combination c + ON c2v.fk_prod_combination = c.rowid + LEFT JOIN ".MAIN_DB_PREFIX."product p ON p.rowid = c.fk_product_child + LEFT JOIN ".MAIN_DB_PREFIX."product_attribute a ON a.rowid = fk_prod_attr +WHERE c.fk_product_parent = ".(int) $productid." AND p.tosell = 1"; + + $sql .= $this->db->order('a.rang', 'asc'); + + $query = $this->db->query($sql); + + //Values + while ($result = $this->db->fetch_object($query)) { + $attr = new ProductAttribute($this->db); + $attr->fetch($result->fk_prod_attr); + + $tmp = new stdClass(); + $tmp->id = $attr->id; + $tmp->ref = $attr->ref; + $tmp->label = $attr->label; + $tmp->values = array(); + + $attrval = new ProductAttributeValue($this->db); + foreach ($res = $attrval->fetchAllByProductAttribute($attr->id, true) as $val) { + $tmp->values[] = $val; + } + + $attributes[] = $tmp; + } + + return $attributes; + } + + /** + * Creates a product combination. Check usages to find more about its use + * + * Format of $combinations array: + * array( + * 0 => array( + * attr => value, + * attr2 => value + * [...] + * ), + * [...] + * ) + * + * @param Product $product Parent product + * @param array $combinations Attribute and value combinations. + * @param array $variations Price and weight variations + * @param bool $price_var_percent Is the price variation a relative variation? + * @param bool|float $forced_pricevar If the price variation is forced + * @param bool|float $forced_weightvar If the weight variation is forced + * @return int <0 KO, >0 OK + */ + public static function createProductCombination(Product $product, array $combinations, array $variations, $price_var_percent = false, $forced_pricevar = false, $forced_weightvar = false) + { + global $db, $user; + + require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductAttribute.class.php'; + require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductAttributeValue.class.php'; + + $db->begin(); + + $newproduct = clone $product; + + //Final weight impact + $weight_impact = $forced_weightvar; + + if ($forced_weightvar === false) { + $weight_impact = 0; + } + + //Final price impact + $price_impact = $forced_pricevar; + + if ($forced_pricevar === false) { + $price_impact = 0; + } + + $newcomb = new ProductCombination($db); + $existingCombination = $newcomb->fetchByProductCombination2ValuePairs($product->id, $combinations); + + if ($existingCombination) { + $newcomb = $existingCombination; + } else { + $newcomb->fk_product_parent = $product->id; + + if ($newcomb->create() < 0) { + $db->rollback(); + return -1; + } + } + + $prodattr = new ProductAttribute($db); + $prodattrval = new ProductAttributeValue($db); + + foreach ($combinations as $currcombattr => $currcombval) { + + //This was checked earlier, so no need to double check + $prodattr->fetch($currcombattr); + $prodattrval->fetch($currcombval); + + //If there is an existing combination, there is no need to duplicate the valuepair + if (!$existingCombination) { + $tmp = new ProductCombination2ValuePair($db); + $tmp->fk_prod_attr = $currcombattr; + $tmp->fk_prod_attr_val = $currcombval; + $tmp->fk_prod_combination = $newcomb->id; + + if ($tmp->create() < 0) { + $db->rollback(); + return -1; + } + } + + if ($forced_weightvar === false) { + $weight_impact += (float) price2num($variations[$currcombattr][$currcombval]['weight']); + } + if ($forced_pricevar === false) { + $price_impact += (float) price2num($variations[$currcombattr][$currcombval]['price']); + } + + $newproduct->ref .= '_'.$prodattrval->ref; + + //The first one should not contain a linebreak + if ($newproduct->description) { + $newproduct->description .= '
'; + } + $newproduct->description .= ''.$prodattr->label.': '.$prodattrval->value; + } + + $newcomb->variation_price_percentage = $price_var_percent; + $newcomb->variation_price = $price_impact; + $newcomb->variation_weight = $weight_impact; + + $newproduct->weight += $weight_impact; + + //To avoid wrong information in price history log + $newproduct->price = 0; + $newproduct->price_ttc = 0; + $newproduct->price_min = 0; + $newproduct->price_min_ttc = 0; + + if ($newproduct->create($user) < 0) { + + //In case the error is not related with an already existing product + if ($newproduct->error != 'ErrorProductAlreadyExists') { + $db->rollback(); + return -1; + } + + /** + * If there is an existing combination, then we update the prices and weight + * Otherwise, we try adding a random number to the ref + */ + + if ($newcomb->fk_product_child) { + $res = $newproduct->fetch($existingCombination->fk_product_child); + } else { + $orig_prod_ref = $newproduct->ref; + $i = 1; + + do { + $newproduct->ref = $orig_prod_ref.$i; + $res = $newproduct->create($user); + + if ($newproduct->error != 'ErrorProductAlreadyExists') { + break; + } + + $i++; + } while ($res < 0); + } + + if ($res < 0) { + $db->rollback(); + return -1; + } + + $newproduct->weight += $weight_impact; + } + + $newcomb->fk_product_child = $newproduct->id; + + if ($newcomb->update() < 0) { + $db->rollback(); + return -1; + } + + $db->commit(); + return 1; + } + + /** + * Copies all product combinations from the origin product to the destination product + * + * @param int $origProductId Origin product id + * @param Product $destProduct Destination product + * @return int >0 OK <0 KO + */ + public function copyAll($origProductId, Product $destProduct) + { + require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductCombination2ValuePair.class.php'; + + //To prevent a loop + if ($origProductId == $destProduct->id) { + return -1; + } + + $prodcomb = new ProductCombination($this->db); + $prodcomb2val = new ProductCombination2ValuePair($this->db); + + //Retrieve all product combinations + $combinations = $prodcomb->fetchAllByFkProductParent($origProductId); + + foreach ($combinations as $combination) { + + $variations = array(); + + foreach ($prodcomb2val->fetchByFkCombination($combination->id) as $tmp_pc2v) { + $variations[$tmp_pc2v->fk_prod_attr] = $tmp_pc2v->fk_prod_attr_val; + } + + if (self::createProductCombination( + $destProduct, + $variations, + array(), + $combination->variation_price_percentage, + $combination->variation_price, + $combination->variation_weight + ) < 0) { + return -1; + } + } + + return 1; + } +} \ No newline at end of file diff --git a/htdocs/attributes/class/ProductCombination2ValuePair.class.php b/htdocs/attributes/class/ProductCombination2ValuePair.class.php new file mode 100644 index 00000000000..01bd0952d2d --- /dev/null +++ b/htdocs/attributes/class/ProductCombination2ValuePair.class.php @@ -0,0 +1,155 @@ + + * + * 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 . + */ + +/** + * Class ProductCombination2ValuePair + * Used to represent the relation between a product combination, a product attribute and a product attribute value + */ +class ProductCombination2ValuePair +{ + /** + * Database handler + * @var DoliDB + */ + private $db; + + /** + * Combination 2 value pair id + * @var int + */ + public $id; + + /** + * Product combination id + * @var int + */ + public $fk_prod_combination; + + /** + * Product attribute id + * @var int + */ + public $fk_prod_attr; + + /** + * Product attribute value id + * @var int + */ + public $fk_prod_attr_val; + + public function __construct(DoliDB $db) + { + $this->db = $db; + } + + /** + * Translates this class to a human-readable string + * + * @return string + */ + public function __toString() + { + require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductAttributeValue.class.php'; + require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductAttribute.class.php'; + + $prodattr = new ProductAttribute($this->db); + $prodattrval = new ProductAttributeValue($this->db); + + $prodattr->fetch($this->fk_prod_attr); + $prodattrval->fetch($this->fk_prod_attr_val); + + return $prodattr->label.': '.$prodattrval->value; + } + + /** + * Creates a product combination 2 value pair + * @return int <0 KO, >0 OK + */ + public function create() + { + $sql = "INSERT INTO ".MAIN_DB_PREFIX."product_attribute_combination2val + (fk_prod_combination, fk_prod_attr, fk_prod_attr_val) + VALUES(".(int) $this->fk_prod_combination.", ".(int) $this->fk_prod_attr.", ".(int) $this->fk_prod_attr_val.")"; + + $query = $this->db->query($sql); + + if ($query) { + $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.'product_attribute_combination2val'); + + return 1; + } + + return -1; + } + + /** + * Retrieves a product combination 2 value pair from its rowid + * + * @param int $fk_combination Fk combination to search + * @return int|ProductCombination2ValuePair[] -1 if KO + */ + public function fetchByFkCombination($fk_combination) + { + $sql = "SELECT + c.rowid, + c2v.fk_prod_attr_val, + c2v.fk_prod_attr, + c2v.fk_prod_combination +FROM ".MAIN_DB_PREFIX."product_attribute c LEFT JOIN ".MAIN_DB_PREFIX."product_attribute_combination2val c2v ON c.rowid = c2v.fk_prod_attr +WHERE c2v.fk_prod_combination = ".(int) $fk_combination; + + $sql .= $this->db->order('c.rang', 'asc'); + + $query = $this->db->query($sql); + + if (!$query) { + return -1; + } + + $return = array(); + + while ($result = $this->db->fetch_object($query)) { + $tmp = new ProductCombination2ValuePair($this->db); + $tmp->fk_prod_attr_val = $result->fk_prod_attr_val; + $tmp->fk_prod_attr = $result->fk_prod_attr; + $tmp->fk_prod_combination = $result->fk_prod_combination; + $tmp->id = $result->rowid; + + $return[] = $tmp; + } + + return $return; + } + + /** + * Deletes a product combination 2 value pair + * + * @param int $fk_combination Rowid of the combination + * @return int >0 OK <0 KO + */ + public function deleteByFkCombination($fk_combination) + { + $sql = "DELETE FROM ".MAIN_DB_PREFIX."product_attribute_combination2val WHERE fk_prod_combination = ".(int) $fk_combination; + + if ($this->db->query($sql)) { + return 1; + } + + return -1; + } +} \ No newline at end of file diff --git a/htdocs/attributes/class/index.html b/htdocs/attributes/class/index.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/htdocs/attributes/combinations.php b/htdocs/attributes/combinations.php new file mode 100644 index 00000000000..b8a758c2341 --- /dev/null +++ b/htdocs/attributes/combinations.php @@ -0,0 +1,641 @@ + + * + * 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 . + */ + +require '../main.inc.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; +require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductAttribute.class.php'; +require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductAttributeValue.class.php'; +require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductCombination.class.php'; +require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductCombination2ValuePair.class.php'; + +$langs->load("products"); +$langs->load("other"); + +$var = false; +$id = GETPOST('id', 'int'); +$valueid = GETPOST('valueid', 'int'); +$ref = GETPOST('ref'); +$weight_impact = (float) GETPOST('weight_impact'); +$price_impact = (float) GETPOST('price_impact'); +$price_impact_percent = (bool) GETPOST('price_impact_percent'); +$form = new Form($db); +$action = GETPOST('action'); + +// Security check +$fieldvalue = (! empty($id) ? $id : $ref); +$fieldtype = (! empty($ref) ? 'ref' : 'rowid'); +$result=restrictedArea($user,'produit|service',$fieldvalue,'product&product','','',$fieldtype); + +$prodstatic = new Product($db); +$prodattr = new ProductAttribute($db); +$prodattr_val = new ProductAttributeValue($db); + +$product = new Product($db); + +$product->fetch($id); + +if (!$product->isProduct()) { + header('Location: '.dol_buildpath('/product/card.php?id='.$product->id, 2)); + die; +} + +$prodcomb = new ProductCombination($db); +$prodcomb2val = new ProductCombination2ValuePair($db); + +$productCombination2ValuePairs1 = array(); + +if ($_POST) { + + if ($action == 'add') { + + $features = GETPOST('features', 'array'); + + if (!$features) { + setEventMessage($langs->trans('ErrorFieldsRequired'), 'errors'); + } else { + $weight_impact = price2num($weight_impact); + $price_impact = price2num($price_impact); + $sanit_features = array(); + + //First, sanitize + foreach ($features as $feature) { + + $explode = explode(':', $feature); + + if ($prodattr->fetch($explode[0]) < 0) { + continue; + } + + if ($prodattr_val->fetch($explode[1]) < 0) { + continue; + } + + //Valuepair + $sanit_features[$explode[0]] = $explode[1]; + + $tmp = new ProductCombination2ValuePair($db); + $tmp->fk_prod_attr = $explode[0]; + $tmp->fk_prod_attr_val = $explode[1]; + + $productCombination2ValuePairs1[] = $tmp; + } + + $db->begin(); + + if (!$prodcomb->fetchByProductCombination2ValuePairs($id, $sanit_features)) { + if (ProductCombination::createProductCombination($product, $sanit_features, array(), $price_impact_percent, $price_impact, $weight_impact)) { + $db->commit(); + setEventMessage($langs->trans('RecordSaved')); + header('Location: '.dol_buildpath('/attributes/combinations.php?id='.$id, 2)); + die; + } else { + setEventMessage($langs->trans('CoreErrorMessage'), 'errors'); + } + } else { + setEventMessage($langs->trans('ErrorRecordAlreadyExists'), 'errors'); + } + + $db->rollback(); + } + } elseif ($action == 'bulk_actions') { + + $prodarray = array_keys(GETPOST('select', 'array')); + $bulkaction = GETPOST('bulk_action'); + $error = 0; + + $prodstatic = new Product($db); + + $db->begin(); + + foreach ($prodarray as $prodid) { + + if ($prodstatic->fetch($prodid) < 0) { + continue; + } + + if ($bulkaction == 'on_sell') { + $prodstatic->status = 1; + $res = $prodstatic->update($prodstatic->id, $user); + } elseif ($bulkaction == 'on_buy') { + $prodstatic->status_buy = 1; + $res = $prodstatic->update($prodstatic->id, $user); + } elseif ($bulkaction == 'not_sell') { + $prodstatic->status = 0; + $res = $prodstatic->update($prodstatic->id, $user); + } elseif ($bulkaction == 'not_buy') { + $prodstatic->status_buy = 0; + $res = $prodstatic->update($prodstatic->id, $user); + } elseif ($bulkaction == 'delete') { + $res = $prodstatic->delete($prodstatic->id); + } else { + break; + } + + if ($res <= 0) { + $error++; + break; + } + } + + if ($error) { + $db->rollback(); + + if ($prodstatic->error) { + setEventMessage($langs->trans($prodstatic->error), 'errors'); + } else { + setEventMessage($langs->trans('CoreErrorMessage'), 'errors'); + } + + } else { + $db->commit(); + setEventMessage($langs->trans('RecordSaved')); + } + + } else { + + if ($prodcomb->fetch($valueid) < 0) { + dol_print_error($db, $langs->trans('ErrorRecordNotFound')); + die; + } + + $prodcomb->variation_price_percentage = $price_impact_percent; + $prodcomb->variation_price = $price_impact; + $prodcomb->variation_weight = $weight_impact; + + if ($prodcomb->update() > 0) { + setEventMessage($langs->trans('RecordSaved')); + header('Location: '.dol_buildpath('/attributes/combinations.php?id='.$id, 2)); + die; + } else { + setEventMessage($langs->trans('CoreErrorMessage'), 'errors'); + } + } +} + +$productCombinations = $prodcomb->fetchAllByFkProductParent($id); + +if ($action === 'confirm_deletecombination') { + + if ($prodcomb->fetch($valueid) > 0) { + + $db->begin(); + + if ($prodcomb->delete() > 0 && $prodstatic->fetch($prodcomb->fk_product_child) > 0 && $prodstatic->delete() > 0) { + $db->commit(); + setEventMessage($langs->trans('RecordSaved')); + header('Location: '.dol_buildpath('/attributes/combinations.php?id='.$product->id, 2)); + die; + } + + $db->rollback(); + setEventMessage($langs->trans('ProductCombinationAlreadyUsed'), 'errors'); + $action = ''; + } +} elseif ($action === 'edit') { + + if ($prodcomb->fetch($valueid) < 0) { + dol_print_error($db, $langs->trans('ErrorRecordNotFound')); + die; + } + + $weight_impact = $prodcomb->variation_weight; + $price_impact = $prodcomb->variation_price; + $price_impact_percent = $prodcomb->variation_price_percentage; + + $productCombination2ValuePairs1 = $prodcomb2val->fetchByFkCombination($valueid); +} elseif ($action === 'confirm_copycombination') { + + //Check destination product + $dest_product = GETPOST('dest_product'); + + if ($prodstatic->fetch('', $dest_product) > 0) { + + //To prevent from copying to the same product + if ($prodstatic->ref != $product->ref) { + if ($prodcomb->copyAll($product->id, $prodstatic) > 0) { + header('Location: '.dol_buildpath('/attributes/combinations.php?id='.$prodstatic->id, 2)); + die; + } else { + setEventMessage($langs->trans('ErrorCopyProductCombinations'), 'errors'); + } + } + + } else { + setEventMessage($langs->trans('ErrorDestinationProductNotFound'), 'errors'); + } + +} + +/* + * View + */ + +if (! empty($id) || ! empty($ref)) { + $object = new Product($db); + $result = $object->fetch($id, $ref); + + llxHeader("", "", $langs->trans("CardProduct".$object->type)); + + if ($result) { + $head = product_prepare_head($object); + $titre = $langs->trans("CardProduct".$object->type); + $picto = ($object->type == Product::TYPE_SERVICE ? 'service' : 'product'); + + dol_fiche_head($head, 'combinations', $titre, 0, $picto); + + print ''; + + // Reference + print ''; + print ''; + print ''; + + // Label + print ''; + + // Status (to sell) + print ''; + + // Status (to buy) + print ''; + + print '
'.$langs->trans("Ref").''; + print $form->showrefnav($object, 'id', '', 0); + print '
'.$langs->trans("Label").''.$object->label.'
'.$langs->trans("Status").' ('.$langs->trans("Sell").')'; + print $object->getLibStatut(2, 0); + print '
'.$langs->trans("Status").' ('.$langs->trans("Buy").')'; + print $object->getLibStatut(2, 1); + print '
'; + + dol_fiche_end(); + } + + if ($action == 'add' || ($action == 'edit')) { + + if ($action == 'add') { + $title = $langs->trans('NewProductCombination'); + } else { + $title = $langs->trans('EditProductCombination'); + } + + print_fiche_titre($title); + + if ($action == 'add') { + $prodattr_all = $prodattr->fetchAll(); + + if (!$selected) { + $selected = $prodattr_all[key($prodattr_all)]->id; + } + + $prodattr_alljson = array(); + + foreach ($prodattr_all as $each) { + $prodattr_alljson[$each->id] = $each; + } + + ?> + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +

+ + +
+ >
+ +
+
+ + + +
+fetch($valueid) > 0) { + $form = new Form($db); + $prodstatic->fetch($prodcomb->fk_product_child); + + print $form->formconfirm( + "combinations.php?id=".$id."&valueid=".$valueid, + $langs->trans('Delete'), + $langs->trans('ProductCombinationDeleteDialog', $prodstatic->getNomUrl(1)), + "confirm_deletecombination", + '', + 0, + 1 + ); + } + } elseif ($action === 'copy') { + + $form = new Form($db); + + print $form->formconfirm( + 'combinations.php?id='.$id, + $langs->trans('CloneCombinationsProduct'), + $langs->trans('ConfirmCloneProductCombinations'), + 'confirm_copycombination', + array( + array( + 'type' => 'text', + 'label' => $langs->trans('CloneDestinationReference'), + 'name' => 'dest_product' + ) + ), + 0, + 1 + ); + } + + $comb2val = new ProductCombination2ValuePair($db); + + if ($productCombinations): ?> + + + +
+ + + + +
+
+ + + + + + + + + + + + + + fetch($currcomb->fk_product_child); ?> + > + + + + + + + + + + +
+ + + + trans('Product') ?>trans('Combination') ?>trans('PriceImpact') ?>trans('WeightImpact') ?>trans('OnSell') ?>trans('OnBuy') ?>
getNomUrl(1) ?> + fetchByFkCombination($currcomb->id); + $iMax = count($productCombination2ValuePairs); + + for ($i = 0; $i < $iMax; $i++) { + echo dol_htmlentities($productCombination2ValuePairs[$i]); + + if ($i !== ($iMax - 1)) { + echo ', '; + } + } ?> + variation_price >= 0 ? '+' : '').price($currcomb->variation_price).($currcomb->variation_price_percentage ? ' %' : '') ?>variation_weight >= 0 ? '+' : '').price($currcomb->variation_weight).' '.measuring_units_string($prodstatic->weight_units, 'weight') ?>getLibStatut(2, 0) ?>getLibStatut(2, 1) ?> + + +
+ + +
+ + + '; + print ' '; + print ''; + + } +} + +llxFooter(); diff --git a/htdocs/attributes/create.php b/htdocs/attributes/create.php new file mode 100644 index 00000000000..5b68ed28907 --- /dev/null +++ b/htdocs/attributes/create.php @@ -0,0 +1,73 @@ + + * + * 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 . + */ + +require '../main.inc.php'; +require 'class/ProductAttribute.class.php'; + +$ref = GETPOST('ref'); +$label = GETPOST('label'); + +if ($_POST) { + + if (empty($ref) || empty($label)) { + setEventMessage($langs->trans('ErrorFieldsRequired'), 'errors'); + } else { + + $prodattr = new ProductAttribute($db); + $prodattr->label = $label; + $prodattr->ref = $ref; + + if ($prodattr->create()) { + setEventMessage($langs->trans('RecordSaved')); + header('Location: '.dol_buildpath('/attributes/list.php', 2)); + } else { + setEventMessage($langs->trans('ErrorRecordAlreadyExists'), 'errors'); + } + } +} + +$langs->load('products'); + +$title = $langs->trans('NewProductAttribute'); + +llxHeader('', $title); + +print_fiche_titre($title); + +dol_fiche_head(); + +?> +
+ + + + + + + + + + +
+ +
'; + +llxFooter(); \ No newline at end of file diff --git a/htdocs/attributes/create_val.php b/htdocs/attributes/create_val.php new file mode 100644 index 00000000000..b25e24fa64a --- /dev/null +++ b/htdocs/attributes/create_val.php @@ -0,0 +1,102 @@ + + * + * 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 . + */ + +require '../main.inc.php'; +require 'class/ProductAttribute.class.php'; +require 'class/ProductAttributeValue.class.php'; + +$id = GETPOST('id'); +$ref = GETPOST('ref'); +$value = GETPOST('value'); + +$prodattr = new ProductAttribute($db); +$prodattrval = new ProductAttributeValue($db); + +if ($prodattr->fetch($id) < 1) { + dol_print_error($db, $langs->trans('ErrorRecordNotFound')); + die; +} + +if ($_POST) { + + if (empty($ref) || empty($value)) { + setEventMessage($langs->trans('ErrorFieldsRequired'), 'errors'); + } else { + + $prodattrval->fk_product_attribute = $prodattr->id; + $prodattrval->ref = $ref; + $prodattrval->value = $value; + + if ($prodattrval->create() > 0) { + setEventMessage($langs->trans('RecordSaved')); + header('Location: '.dol_buildpath('/attributes/card.php?id='.$prodattr->id, 2)); + die; + } else { + setEventMessage($langs->trans('ErrorCreatingProductAttributeValue'), 'errors'); + } + } + +} + +$langs->load('products'); + +$title = $langs->trans('ProductAttributeName', dol_htmlentities($prodattr->label)); + +llxHeader('', $title); + +print_fiche_titre($title); + +dol_fiche_head(); + +?> + + + + + + + + +
trans('Ref') ?>ref) ?> +
trans('Label') ?>label) ?>
+ +trans('NewProductAttributeValue')); + +dol_fiche_head(); +?> +
+ + + + + + + + + +
+
'; + +llxFooter(); \ No newline at end of file diff --git a/htdocs/attributes/generator.php b/htdocs/attributes/generator.php new file mode 100644 index 00000000000..37f2175cc49 --- /dev/null +++ b/htdocs/attributes/generator.php @@ -0,0 +1,393 @@ + + * + * 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 . + */ + +require '../main.inc.php'; +require_once DOL_DOCUMENT_ROOT.'/core/lib/product.lib.php'; +require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; +require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductAttribute.class.php'; +require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductAttributeValue.class.php'; +require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductCombination.class.php'; +require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductCombination2ValuePair.class.php'; + +$langs->load("products"); +$langs->load('other'); + +$id = GETPOST('id', 'int'); +$ref = GETPOST('ref'); +$form = new Form($db); + +// Security check +$fieldvalue = (! empty($id) ? $id : $ref); +$fieldtype = (! empty($ref) ? 'ref' : 'rowid'); +$result=restrictedArea($user,'produit|service',$fieldvalue,'product&product','','',$fieldtype); + +$prodattr = new ProductAttribute($db); +$prodattrval = new ProductAttributeValue($db); +$product = new Product($db); + +$product->fetch($id); + +if (!$product->isProduct()) { + header('Location: '.dol_buildpath('/product/card.php?id='.$product->id, 2)); + die; +} + +/** + * Posible combinations. Format. + * attrval => array( + * valueid => array( + * price => '' + * weight => '' + * ) + * ) + */ +$combinations = GETPOST('combinations', 'array'); +$price_var_percent = (bool) GETPOST('price_var_percent'); +$donotremove = true; + +if ($_POST) { + + $donotremove = (bool) GETPOST('donotremove'); + + //We must check if all those given combinations actually exist + $sanitized_values = array(); + + foreach ($combinations as $attr => $val) { + if ($prodattr->fetch($attr) > 0) { + foreach ($val as $valueid => $content) { + if ($prodattrval->fetch($valueid) > 0) { + $sanitized_values[$attr][$valueid] = $content; + } + } + } + } + + if ($sanitized_values) { + + require DOL_DOCUMENT_ROOT.'/core/lib/functions2.lib.php'; + + $adapted_values = array(); + + //Adapt the array to the cartesian function + foreach ($sanitized_values as $attr => $val) { + $adapted_values[$attr] = array_keys($val); + } + + $db->begin(); + + $combination = new ProductCombination($db); + + $delete_prev_comb_res = 1; + + if (!$donotremove) { + $delete_prev_comb_res = $combination->deleteByFkProductParent($id); + } + + //Current combinations will be deleted + if ($delete_prev_comb_res > 0) { + + $res = 1; + + foreach (cartesianArray($adapted_values) as $currcomb) { + + $res = ProductCombination::createProductCombination($product, $currcomb, $sanitized_values, $price_var_percent); + + if ($res < 0) { + break; + } + } + + if ($res > 0) { + $db->commit(); + setEventMessage($langs->trans('RecordSaved')); + header('Location: '.dol_buildpath('/attributes/combinations.php?id='.$id, 2)); + die; + } else { + setEventMessage($langs->trans('CoreErrorMessage'), 'errors'); + } + } else { + setEventMessage($langs->trans('ErrorDeletingGeneratedProducts'), 'errors'); + } + + $db->rollback(); + + } else { + setEventMessage($langs->trans('ErrorFieldsRequired'), 'errors'); + } +} + +/* + * View + */ + +if (! empty($id) || ! empty($ref)) { + $object = new Product($db); + $result = $object->fetch($id, $ref); + + llxHeader("", "", $langs->trans("CardProduct".$object->type)); + + if ($result) { + $head = product_prepare_head($object); + $titre = $langs->trans("CardProduct".$object->type); + $picto = ($object->type == Product::TYPE_SERVICE ? 'service' : 'product'); + + dol_fiche_head($head, 'combinations', $titre, 0, $picto); + + print ''; + + // Reference + print ''; + print ''; + print ''; + + // Label + print ''; + + // Status (to sell) + print ''; + + // Status (to buy) + print ''; + + print '
'.$langs->trans("Ref").''; + print $form->showrefnav($object, 'id', '', 0); + print '
'.$langs->trans("Label").''.$object->label.'
'.$langs->trans("Status").' ('.$langs->trans("Sell").')'; + print $object->getLibStatut(2, 0); + print '
'.$langs->trans("Status").' ('.$langs->trans("Buy").')'; + print $object->getLibStatut(2, 1); + print '
'; + + dol_fiche_end(); + } + + print_fiche_titre($langs->trans('ProductCombinationGenerator')); + + $dictionnary_attr = array(); + + foreach ($prodattr->fetchAll() as $attr) { + $dictionnary_attr[$attr->id] = $attr; + foreach ($prodattrval->fetchAllByProductAttribute($attr->id) as $attrval) { + $dictionnary_attr[$attr->id]->values[$attrval->id] = $attrval; + } + } + ?> + + + +
+ +
+ +
+ +

trans('TooMuchCombinationsWarning', $langs->trans('DoNotRemovePreviousCombinations')) ?>

+ > +
+ > + +
+
+ + trans('ProductCombinationGeneratorWarning') ?> +
+
+ +
+ +
+ +
+ +
+
+ + + +

+ + + +
+ +
+ + + + * + * 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 . + */ + +/** + * Returns the entity of the products. + * @param DoliDB $db Database handler + * @return mixed + */ +function getProductEntities(DoliDB $db) +{ + require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; + + $product = new Product($db); + + return getEntity($product->element, 1); +} \ No newline at end of file diff --git a/htdocs/attributes/list.php b/htdocs/attributes/list.php new file mode 100644 index 00000000000..3c4cf945524 --- /dev/null +++ b/htdocs/attributes/list.php @@ -0,0 +1,140 @@ + + * + * 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 . + */ + +require '../main.inc.php'; +require 'class/ProductAttribute.class.php'; + +$rowid = GETPOST('rowid'); +$action = GETPOST('action'); +$object = new ProductAttribute($db); + +if ($action == 'up') { + $object->fetch($rowid); + $object->moveUp(); + + header('Location: '.$_SERVER['PHP_SELF']); + die; +} elseif ($action == 'down') { + $object->fetch($rowid); + $object->moveDown(); + + header('Location: '.$_SERVER['PHP_SELF']); + die; +} + +$langs->load('products'); + +$var = false; +$title = $langs->trans($langs->trans('ProductAttributes')); + +$attributes = $object->fetchAll(); + +llxHeader('', $title); + +print_fiche_titre($title); + +$forcereloadpage=empty($conf->global->MAIN_FORCE_RELOAD_PAGE)?0:1; +?> + + + + + + + + + + $attribute): ?> + > + + + + + + + + +
trans('Ref') ?>trans('Label') ?>trans('NbProducts') ?>
ref) ?>label) ?>countChildProducts() ?> + + + + 0): ?> + + + + + +
+ +
+ + + * Copyright (C) 2013-2014 Florian Henry * Copyright (C) 2014 Ferran Marcet + * Copyright (C) 2016 Marcos García * * 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 @@ -50,6 +51,10 @@ if (! empty($conf->projet->enabled)) { require_once DOL_DOCUMENT_ROOT . '/core/class/html.formprojet.class.php'; } +if (!empty($conf->attributes->enabled)) { + require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductCombination.class.php'; +} + $langs->load('companies'); $langs->load('propal'); $langs->load('compta'); @@ -682,7 +687,8 @@ if (empty($reshook)) $product_desc=(GETPOST('dp_desc')?GETPOST('dp_desc'):''); $price_ht = GETPOST('price_ht'); $price_ht_devise = GETPOST('multicurrency_price_ht'); - if (GETPOST('prod_entry_mode') == 'free') + $prod_entry_mode = GETPOST('prod_entry_mode'); + if ($prod_entry_mode == 'free') { $idprod=0; $tva_tx = (GETPOST('tva_tx') ? GETPOST('tva_tx') : 0); @@ -708,21 +714,35 @@ if (empty($reshook)) } } - if (GETPOST('prod_entry_mode') == 'free' && empty($idprod) && GETPOST('type') < 0) { + if ($prod_entry_mode == 'free' && empty($idprod) && GETPOST('type') < 0) { setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Type")), null, 'errors'); $error ++; } - if (GETPOST('prod_entry_mode') == 'free' && empty($idprod) && $price_ht == '' && $price_ht_devise == '') // Unit price can be 0 but not ''. Also price can be negative for proposal. + if ($prod_entry_mode == 'free' && empty($idprod) && $price_ht == '' && $price_ht_devise == '') // Unit price can be 0 but not ''. Also price can be negative for proposal. { setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("UnitPriceHT")), null, 'errors'); $error ++; } - if (GETPOST('prod_entry_mode') == 'free' && empty($idprod) && empty($product_desc)) { + if ($prod_entry_mode == 'free' && empty($idprod) && empty($product_desc)) { setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("Description")), null, 'errors'); $error ++; } + if (!$error && !empty($conf->attributes->enabled) && $prod_entry_mode != 'free') { + if ($combinations = GETPOST('combinations', 'array')) { + //Check if there is a product with the given combination + $prodcomb = new ProductCombination($db); + + if ($res = $prodcomb->fetchByProductCombination2ValuePairs($idprod, $combinations)) { + $idprod = $res->fk_product_child; + } else { + setEventMessage($langs->trans('ErrorProductCombinationNotFound'), 'errors'); + $error ++; + } + } + } + if (! $error && ($qty >= 0) && (! empty($product_desc) || ! empty($idprod))) { $pu_ht = 0; $pu_ttc = 0; @@ -961,7 +981,7 @@ if (empty($reshook)) // Add buying price $fournprice = price2num(GETPOST('fournprice') ? GETPOST('fournprice') : ''); $buyingprice = price2num(GETPOST('buying_price') != '' ? GETPOST('buying_price') : ''); // If buying_price is '0', we muste keep this value - + $pu_ht_devise = GETPOST('multicurrency_subprice'); $date_start = dol_mktime(GETPOST('date_starthour'), GETPOST('date_startmin'), GETPOST('date_startsec'), GETPOST('date_startmonth'), GETPOST('date_startday'), GETPOST('date_startyear')); diff --git a/htdocs/commande/card.php b/htdocs/commande/card.php index f7917b13839..4f049a1fe4e 100644 --- a/htdocs/commande/card.php +++ b/htdocs/commande/card.php @@ -7,7 +7,7 @@ * Copyright (C) 2010-2013 Juanjo Menent * Copyright (C) 2011-2016 Philippe Grand * Copyright (C) 2012-2013 Christophe Battarel - * Copyright (C) 2012 Marcos García + * Copyright (C) 2012-2016 Marcos García * Copyright (C) 2012 Cedric Salvador * Copyright (C) 2013 Florian Henry * Copyright (C) 2014 Ferran Marcet @@ -51,6 +51,10 @@ if (! empty($conf->projet->enabled)) { } require_once DOL_DOCUMENT_ROOT . '/core/class/doleditor.class.php'; +if (!empty($conf->attributes->enabled)) { + require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductCombination.class.php'; +} + $langs->load('orders'); $langs->load('sendings'); $langs->load('companies'); @@ -640,7 +644,8 @@ if (empty($reshook)) $product_desc=(GETPOST('dp_desc')?GETPOST('dp_desc'):''); $price_ht = GETPOST('price_ht'); $price_ht_devise = GETPOST('multicurrency_price_ht'); - if (GETPOST('prod_entry_mode') == 'free') + $prod_entry_mode = GETPOST('prod_entry_mode'); + if ($prod_entry_mode == 'free') { $idprod=0; $tva_tx = (GETPOST('tva_tx') ? GETPOST('tva_tx') : 0); @@ -670,11 +675,11 @@ if (empty($reshook)) setEventMessages($langs->trans('ErrorBothFieldCantBeNegative', $langs->transnoentitiesnoconv('UnitPriceHT'), $langs->transnoentitiesnoconv('Qty')), null, 'errors'); $error++; } - if (GETPOST('prod_entry_mode') == 'free' && empty($idprod) && GETPOST('type') < 0) { + if ($prod_entry_mode == 'free' && empty($idprod) && GETPOST('type') < 0) { setEventMessages($langs->trans('ErrorFieldRequired', $langs->transnoentitiesnoconv('Type')), null, 'errors'); $error++; } - if (GETPOST('prod_entry_mode') == 'free' && empty($idprod) && (! ($price_ht >= 0) || $price_ht == '') && (! ($price_ht_devise >= 0) || $price_ht_devise == '')) // Unit price can be 0 but not '' + if ($prod_entry_mode == 'free' && empty($idprod) && (! ($price_ht >= 0) || $price_ht == '') && (! ($price_ht_devise >= 0) || $price_ht_devise == '')) // Unit price can be 0 but not '' { setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("UnitPriceHT")), null, 'errors'); $error++; @@ -683,11 +688,25 @@ if (empty($reshook)) setEventMessages($langs->trans('ErrorFieldRequired', $langs->transnoentitiesnoconv('Qty')), null, 'errors'); $error++; } - if (GETPOST('prod_entry_mode') == 'free' && empty($idprod) && empty($product_desc)) { + if ($prod_entry_mode == 'free' && empty($idprod) && empty($product_desc)) { setEventMessages($langs->trans('ErrorFieldRequired', $langs->transnoentitiesnoconv('Description')), null, 'errors'); $error++; } + if (!$error && !empty($conf->attributes->enabled) && $prod_entry_mode != 'free') { + if ($combinations = GETPOST('combinations', 'array')) { + //Check if there is a product with the given combination + $prodcomb = new ProductCombination($db); + + if ($res = $prodcomb->fetchByProductCombination2ValuePairs($idprod, $combinations)) { + $idprod = $res->fk_product_child; + } else { + setEventMessage($langs->trans('ErrorProductCombinationNotFound'), 'errors'); + $error ++; + } + } + } + if (! $error && ($qty >= 0) && (! empty($product_desc) || ! empty($idprod))) { // Clean parameters $date_start=dol_mktime(GETPOST('date_start'.$predef.'hour'), GETPOST('date_start'.$predef.'min'), GETPOST('date_start'.$predef.'sec'), GETPOST('date_start'.$predef.'month'), GETPOST('date_start'.$predef.'day'), GETPOST('date_start'.$predef.'year')); @@ -2419,7 +2438,7 @@ if ($action == 'create' && $user->rights->commande->creer) } print ''; print ''; - + print "\n"; dol_fiche_end(); diff --git a/htdocs/compta/facture.php b/htdocs/compta/facture.php index f245e259c55..f0ddace3e2e 100644 --- a/htdocs/compta/facture.php +++ b/htdocs/compta/facture.php @@ -13,7 +13,7 @@ * Copyright (C) 2013-2014 Florian Henry * Copyright (C) 2013 Cédric Salvador * Copyright (C) 2014 Ferran Marcet - * Copyright (C) 2015 Marcos García + * Copyright (C) 2015-2016 Marcos García * * 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 @@ -56,6 +56,10 @@ if (! empty($conf->projet->enabled)) { } require_once DOL_DOCUMENT_ROOT . '/core/class/doleditor.class.php'; +if (!empty($conf->attributes->enabled)) { + require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductCombination.class.php'; +} + $langs->load('bills'); $langs->load('companies'); $langs->load('compta'); @@ -1362,7 +1366,8 @@ if (empty($reshook)) $product_desc=(GETPOST('dp_desc')?GETPOST('dp_desc'):''); $price_ht = GETPOST('price_ht'); $price_ht_devise = GETPOST('multicurrency_price_ht'); - if (GETPOST('prod_entry_mode') == 'free') + $prod_entry_mode = GETPOST('prod_entry_mode'); + if ($prod_entry_mode == 'free') { $idprod=0; $tva_tx = (GETPOST('tva_tx') ? GETPOST('tva_tx') : 0); @@ -1392,7 +1397,7 @@ if (empty($reshook)) setEventMessages($langs->trans('ErrorBothFieldCantBeNegative', $langs->transnoentitiesnoconv('UnitPriceHT'), $langs->transnoentitiesnoconv('Qty')), null, 'errors'); $error ++; } - if (! GETPOST('prod_entry_mode')) + if (!$prod_entry_mode) { if (GETPOST('type') < 0 && ! GETPOST('search_idprod')) { @@ -1400,11 +1405,11 @@ if (empty($reshook)) $error ++; } } - if (GETPOST('prod_entry_mode') == 'free' && empty($idprod) && GETPOST('type') < 0) { + if ($prod_entry_mode == 'free' && empty($idprod) && GETPOST('type') < 0) { setEventMessages($langs->trans('ErrorFieldRequired', $langs->transnoentitiesnoconv('Type')), null, 'errors'); $error ++; } - if (GETPOST('prod_entry_mode') == 'free' && empty($idprod) && (! ($price_ht >= 0) || $price_ht == '') && $price_ht_devise == '') // Unit price can be 0 but not '' + if ($prod_entry_mode == 'free' && empty($idprod) && (! ($price_ht >= 0) || $price_ht == '') && $price_ht_devise == '') // Unit price can be 0 but not '' { setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("UnitPriceHT")), null, 'errors'); $error ++; @@ -1413,7 +1418,7 @@ if (empty($reshook)) setEventMessages($langs->trans('ErrorFieldRequired', $langs->transnoentitiesnoconv('Qty')), null, 'errors'); $error ++; } - if (GETPOST('prod_entry_mode') == 'free' && empty($idprod) && empty($product_desc)) { + if ($prod_entry_mode == 'free' && empty($idprod) && empty($product_desc)) { setEventMessages($langs->trans('ErrorFieldRequired', $langs->transnoentitiesnoconv('Description')), null, 'errors'); $error ++; } @@ -1422,7 +1427,23 @@ if (empty($reshook)) setEventMessages($langs->trans('ErrorQtyForCustomerInvoiceCantBeNegative'), null, 'errors'); $error ++; } + + if (!$error && !empty($conf->attributes->enabled) && $prod_entry_mode != 'free') { + if ($combinations = GETPOST('combinations', 'array')) { + //Check if there is a product with the given combination + $prodcomb = new ProductCombination($db); + + if ($res = $prodcomb->fetchByProductCombination2ValuePairs($idprod, $combinations)) { + $idprod = $res->fk_product_child; + } else { + setEventMessage($langs->trans('ErrorProductCombinationNotFound'), 'errors'); + $error ++; + } + } + } + if (! $error && ($qty >= 0) && (! empty($product_desc) || ! empty($idprod))) { + $ret = $object->fetch($id); if ($ret < 0) { dol_print_error($db, $object->error); @@ -1755,7 +1776,7 @@ if (empty($reshook)) } } } - + $result = $object->updateline(GETPOST('lineid'), $description, $pu_ht, $qty, GETPOST('remise_percent'), $date_start, $date_end, $vat_rate, $localtax1_rate, $localtax2_rate, 'HT', $info_bits, $type, GETPOST('fk_parent_line'), 0, $fournprice, $buyingprice, $label, $special_code, $array_options, GETPOST('progress'), @@ -2147,7 +2168,7 @@ if ($action == 'create') print '' . "\n"; $exampletemplateinvoice=new FactureRec($db); - + // Overwrite value if creation of invoice is from a predefined invoice if (empty($origin) && empty($originid) && GETPOST('fac_rec','int') > 0) { @@ -2464,7 +2485,7 @@ if ($action == 'create') } $datefacture = dol_mktime(12, 0, 0, $_POST['remonth'], $_POST['reday'], $_POST['reyear']); - + // Date invoice print '' . $langs->trans('DateInvoice') . ''; print $form->select_date($datefacture?$datefacture:$dateinvoice, '', '', '', '', "add", 1, 1, 1); @@ -2570,15 +2591,15 @@ if ($action == 'create') '__INVOICE_YEAR__' => $langs->trans("PreviousYearOfInvoice").' ('.$langs->trans("Example").': '.dol_print_date($dateexample,'%Y').')', '__INVOICE_NEXT_YEAR__' => $langs->trans("NextYearOfInvoice").' ('.$langs->trans("Example").': '.dol_print_date(dol_time_plus_duree($dateexample, 1, 'y'),'%Y').')' ); - + $htmltext = ''.$langs->trans("FollowingConstantsWillBeSubstituted").':
'; foreach($substitutionarray as $key => $val) { $htmltext.=$key.' = '.$langs->trans($val).'
'; } - $htmltext.='
'; + $htmltext.=''; } - + // Public note print ''; print ''; @@ -3501,7 +3522,7 @@ else if ($id > 0 || ! empty($ref)) print ''; - + // List of previous situation invoices $sign = 1; @@ -3613,9 +3634,9 @@ else if ($id > 0 || ! empty($ref)) print ''; } - + // List of payments already done - + print ''; print ''; @@ -3924,7 +3945,7 @@ else if ($id > 0 || ! empty($ref)) print "
\n"; print ""; - + print "\n"; dol_fiche_end(); diff --git a/htdocs/contrat/card.php b/htdocs/contrat/card.php index 3c88eb64e26..bea5af7e795 100644 --- a/htdocs/contrat/card.php +++ b/htdocs/contrat/card.php @@ -7,7 +7,7 @@ * Copyright (C) 2013 Christophe Battarel * Copyright (C) 2013-2014 Florian Henry * Copyright (C) 2014-2016 Ferran Marcet - * Copyright (C) 2014 Marcos García + * Copyright (C) 2014-2016 Marcos García * Copyright (C) 2015 Jean-François Ferry * * This program is free software; you can redistribute it and/or modify @@ -446,7 +446,7 @@ if (empty($reshook)) unset($_POST["options_" . $key]); } } - + if (! $error) { // Clean parameters diff --git a/htdocs/core/class/html.form.class.php b/htdocs/core/class/html.form.class.php index 6a3f3825c7c..cec7aa2aede 100644 --- a/htdocs/core/class/html.form.class.php +++ b/htdocs/core/class/html.form.class.php @@ -12,7 +12,7 @@ * Copyright (C) 2010 Juanjo Menent * Copyright (C) 2010-2014 Philippe Grand * Copyright (C) 2011 Herve Prot - * Copyright (C) 2012-2014 Marcos García + * Copyright (C) 2012-2016 Marcos García * Copyright (C) 2012 Cedric Salvador * Copyright (C) 2012-2015 Raphaël Doursenaud * Copyright (C) 2014 Alexandre Spangaro @@ -80,7 +80,7 @@ class Form * @param string $typeofdata Type of data ('string' by default, 'email', 'amount:99', 'numeric:99', 'text' or 'textarea:rows:cols', 'datepicker' ('day' do not work, don't know why), 'ckeditor:dolibarr_zzz:width:height:savemethod:1:rows:cols', 'select;xxx[:class]'...) * @param string $moreparam More param to add on a href URL. * @param int $fieldrequired 1 if we want to show field as mandatory using the "fieldrequired" CSS. - * @param int $notabletag 1=Do not output table tags but output a ':', 2=Do not output table tags and no ':', 3=Do not output table tags but output a ' ' + * @param int $notabletag 1=Do not output table tags but output a ':', 2=Do not output table tags and no ':', 3=Do not output table tags but output a ' ' * @return string HTML edit field */ function editfieldkey($text, $htmlname, $preselected, $object, $perm, $typeofdata='string', $moreparam='', $fieldrequired=0, $notabletag=0) @@ -512,7 +512,7 @@ class Form * * @param string $selected Value auto selected when at least one record is selected. Not a preselected value. Use '0' by default. * @param int $arrayofaction array('code'=>'label', ...). The code is the key stored into the GETPOST('massaction') when submitting action. - * @param int $alwaysvisible 1=select button always visible + * @param int $alwaysvisible 1=select button always visible * @return string Select list */ function selectMassAction($selected, $arrayofaction, $alwaysvisible=0) @@ -586,7 +586,7 @@ class Form '; } - + return $ret; } @@ -1410,7 +1410,7 @@ class Form // Build list includeUsers to have only hierarchy and current user $includeUsers = implode(",",$user->getAllChildIds(1)); } - + $out=''; // On recherche les utilisateurs @@ -1616,7 +1616,7 @@ class Form $i++; } if ($nbassignetouser) $out.=''; - + //$out.=''; return $out; } @@ -1642,11 +1642,12 @@ class Form * @param int $hidepriceinlabel 1=Hide prices in label * @param string $warehouseStatus warehouse status filter, following comma separated filter options can be used * 'warehouseopen' = select products from open warehouses, - * 'warehouseclosed' = select products from closed warehouses, + * 'warehouseclosed' = select products from closed warehouses, * 'warehouseinternal' = select products from warehouses for internal correct/transfer only + * @param array $selected_combinations Selected combinations. Format: array([attrid] => attrval, [...]) * @return void */ - function select_produits($selected='', $htmlname='productid', $filtertype='', $limit=20, $price_level=0, $status=1, $finished=2, $selected_input_value='', $hidelabel=0, $ajaxoptions=array(), $socid=0, $showempty='1', $forcecombo=0, $morecss='', $hidepriceinlabel=0, $warehouseStatus='') + function select_produits($selected='', $htmlname='productid', $filtertype='', $limit=20, $price_level=0, $status=1, $finished=2, $selected_input_value='', $hidelabel=0, $ajaxoptions=array(), $socid=0, $showempty='1', $forcecombo=0, $morecss='', $hidepriceinlabel=0, $warehouseStatus='', $selected_combinations = array()) { global $langs,$conf; @@ -1671,6 +1672,80 @@ class Form $urloption.='&socid='.$socid; } print ajax_autocompleter($selected, $htmlname, DOL_URL_ROOT.'/product/ajax/products.php', $urloption, $conf->global->PRODUIT_USE_SEARCH_TO_SELECT, 0, $ajaxoptions); + + if (!empty($conf->attributes->enabled)) { + ?> + + trans("RefOrLabel").' : '; else if ($hidelabel > 1) { if (! empty($conf->global->MAIN_HTML5_PLACEHOLDER)) $placeholder=' placeholder="'.$langs->trans("RefOrLabel").'"'; @@ -1709,7 +1784,7 @@ class Form * @param int $hidepriceinlabel 1=Hide prices in label * @param string $warehouseStatus warehouse status filter, following comma separated filter options can be used * 'warehouseopen' = select products from open warehouses, - * 'warehouseclosed' = select products from closed warehouses, + * 'warehouseclosed' = select products from closed warehouses, * 'warehouseinternal' = select products from warehouses for internal correct/transfer only * @return array Array of keys for json */ @@ -1724,19 +1799,19 @@ class Form if (! empty($warehouseStatus)) { require_once DOL_DOCUMENT_ROOT.'/product/stock/class/entrepot.class.php'; - if (preg_match('/warehouseclosed/', $warehouseStatus)) + if (preg_match('/warehouseclosed/', $warehouseStatus)) { $warehouseStatusArray[] = Entrepot::STATUS_CLOSED; } - if (preg_match('/warehouseopen/', $warehouseStatus)) + if (preg_match('/warehouseopen/', $warehouseStatus)) { $warehouseStatusArray[] = Entrepot::STATUS_OPEN_ALL; } - if (preg_match('/warehouseinternal/', $warehouseStatus)) + if (preg_match('/warehouseinternal/', $warehouseStatus)) { $warehouseStatusArray[] = Entrepot::STATUS_OPEN_INTERNAL; } - } + } $selectFields = " p.rowid, p.label, p.ref, p.description, p.barcode, p.fk_product_type, p.price, p.price_ttc, p.price_base_type, p.tva_tx, p.duration, p.fk_price_expression"; (count($warehouseStatusArray)) ? $selectFieldsGrouped = ", sum(ps.reel) as stock" : $selectFieldsGrouped = ", p.stock"; @@ -1775,7 +1850,7 @@ class Form $sql.= " LEFT JOIN ".MAIN_DB_PREFIX."product_stock as ps on ps.fk_product = p.rowid"; $sql.= " LEFT JOIN ".MAIN_DB_PREFIX."entrepot as e on ps.fk_entrepot = e.rowid"; } - + //Price by customer if (! empty($conf->global->PRODUIT_CUSTOMER_PRICES) && !empty($socid)) { $sql.=" LEFT JOIN ".MAIN_DB_PREFIX."product_customer_price as pcp ON pcp.fk_soc=".$socid." AND pcp.fk_product=p.rowid"; @@ -1785,11 +1860,21 @@ class Form { $sql.= " LEFT JOIN ".MAIN_DB_PREFIX."product_lang as pl ON pl.fk_product = p.rowid AND pl.lang='". $langs->getDefaultLang() ."'"; } + + if (!empty($conf->global->PRODUIT_ATTRIBUTES_HIDECHILD)) { + $sql .= " LEFT JOIN llx_product_attribute_combination pac ON pac.fk_product_child = p.rowid"; + } + $sql.= ' WHERE p.entity IN ('.getEntity('product', 1).')'; if (count($warehouseStatusArray)) { $sql.= ' AND (p.fk_product_type = 1 OR e.statut IN ('.implode(',',$warehouseStatusArray).'))'; } + + if (!empty($conf->global->PRODUIT_ATTRIBUTES_HIDECHILD)) { + $sql .= " AND pac.rowid IS NULL"; + } + if ($finished == 0) { $sql.= " AND p.finished = ".$finished; @@ -1842,7 +1927,7 @@ class Form $num = $this->db->num_rows($result); $events=null; - + if ($conf->use_javascript_ajax && ! $forcecombo) { include_once DOL_DOCUMENT_ROOT . '/core/lib/ajax.lib.php'; @@ -1850,9 +1935,9 @@ class Form $out.= $comboenhancement; $nodatarole=($comboenhancement?' data-role="none"':''); } - + $out.=''; if ($addempty) print ''; foreach($this->cache_conditions_paiements as $id => $arrayconditions) @@ -3385,7 +3470,7 @@ class Form // Clean parameters $newselectedchoice=empty($selectedchoice)?"no":$selectedchoice; if ($conf->browser->layout == 'phone') $width='95%'; - + if (is_array($formquestion) && ! empty($formquestion)) { // First add hidden fields and value @@ -3651,7 +3736,7 @@ class Form require_once DOL_DOCUMENT_ROOT.'/core/class/html.formprojet.class.php'; $out=''; - + $formproject=new FormProjets($this->db); $langs->load("project"); @@ -3679,8 +3764,8 @@ class Form $out.=" "; } } - - if (empty($nooutput)) + + if (empty($nooutput)) { print $out; return ''; @@ -4235,7 +4320,7 @@ class Form // Make select dynamic include_once DOL_DOCUMENT_ROOT . '/core/lib/ajax.lib.php'; $out.= ajax_combobox($htmlname); - + return $out; } @@ -4430,13 +4515,13 @@ class Form $key.= $rate['nprtva'] ? '*': ''; if ($mode > 0 && $rate['code']) $key.=' ('.$rate['code'].')'; if ($mode < 0) $key = $rate['rowid']; - + $return.= '