From c082505021aa6b007b25f8da55e93b9b690ba134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Garci=CC=81a=20de=20La=20Fuente?= Date: Sat, 23 Jul 2016 16:37:21 +0200 Subject: [PATCH 1/5] NEW Added product attributes feature --- htdocs/attributes/admin/admin.php | 65 ++ htdocs/attributes/admin/index.html | 0 htdocs/attributes/ajax/getCombinations.php | 50 ++ .../attributes/ajax/get_attribute_values.php | 61 ++ htdocs/attributes/ajax/index.html | 0 htdocs/attributes/ajax/orderAttribute.php | 53 ++ htdocs/attributes/card.php | 245 +++++++ .../class/ProductAttribute.class.php | 317 +++++++++ .../class/ProductAttributeValue.class.php | 221 ++++++ .../class/ProductCombination.class.php | 641 ++++++++++++++++++ .../ProductCombination2ValuePair.class.php | 151 +++++ htdocs/attributes/class/index.html | 0 htdocs/attributes/combinations.php | 641 ++++++++++++++++++ htdocs/attributes/create.php | 73 ++ htdocs/attributes/create_val.php | 102 +++ htdocs/attributes/generator.php | 393 +++++++++++ htdocs/attributes/index.html | 0 htdocs/attributes/lib/index.html | 0 .../attributes/lib/product_attributes.lib.php | 31 + htdocs/attributes/list.php | 140 ++++ htdocs/comm/propal/card.php | 28 +- htdocs/commande/card.php | 29 +- htdocs/compta/facture.php | 31 +- htdocs/contrat/card.php | 4 +- htdocs/core/class/html.form.class.php | 125 +++- htdocs/core/lib/functions2.lib.php | 29 +- htdocs/core/lib/product.lib.php | 19 +- htdocs/core/tpl/objectline_create.tpl.php | 47 +- htdocs/fourn/commande/card.php | 34 +- htdocs/fourn/facture/card.php | 33 +- .../install/mysql/migration/3.9.0-4.0.0.sql | 1 - .../install/mysql/migration/4.0.0-5.0.0.sql | 36 + ....key.sql => llx_product_attribute.key.sql} | 8 +- ...y_events.sql => llx_product_attribute.sql} | 20 +- .../llx_product_attribute_combination.sql | 28 + .../llx_product_attribute_combination2val.sql | 25 + .../llx_product_attribute_value.key.sql | 19 + .../tables/llx_product_attribute_value.sql | 26 + htdocs/langs/en_US/main.lang | 2 + htdocs/langs/en_US/products.lang | 38 +- htdocs/product/card.php | 18 +- htdocs/product/class/product.class.php | 30 + htdocs/product/list.php | 46 +- 43 files changed, 3763 insertions(+), 97 deletions(-) create mode 100644 htdocs/attributes/admin/admin.php create mode 100644 htdocs/attributes/admin/index.html create mode 100644 htdocs/attributes/ajax/getCombinations.php create mode 100644 htdocs/attributes/ajax/get_attribute_values.php create mode 100644 htdocs/attributes/ajax/index.html create mode 100644 htdocs/attributes/ajax/orderAttribute.php create mode 100644 htdocs/attributes/card.php create mode 100644 htdocs/attributes/class/ProductAttribute.class.php create mode 100644 htdocs/attributes/class/ProductAttributeValue.class.php create mode 100644 htdocs/attributes/class/ProductCombination.class.php create mode 100644 htdocs/attributes/class/ProductCombination2ValuePair.class.php create mode 100644 htdocs/attributes/class/index.html create mode 100644 htdocs/attributes/combinations.php create mode 100644 htdocs/attributes/create.php create mode 100644 htdocs/attributes/create_val.php create mode 100644 htdocs/attributes/generator.php create mode 100644 htdocs/attributes/index.html create mode 100644 htdocs/attributes/lib/index.html create mode 100644 htdocs/attributes/lib/product_attributes.lib.php create mode 100644 htdocs/attributes/list.php rename htdocs/install/mysql/tables/{llx_holiday_events.key.sql => llx_product_attribute.key.sql} (81%) rename htdocs/install/mysql/tables/{llx_holiday_events.sql => llx_product_attribute.sql} (70%) create mode 100644 htdocs/install/mysql/tables/llx_product_attribute_combination.sql create mode 100644 htdocs/install/mysql/tables/llx_product_attribute_combination2val.sql create mode 100644 htdocs/install/mysql/tables/llx_product_attribute_value.key.sql create mode 100644 htdocs/install/mysql/tables/llx_product_attribute_value.sql 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 +{ + /** + * 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..f500fb251c6 --- /dev/null +++ b/htdocs/attributes/class/ProductAttributeValue.class.php @@ -0,0 +1,221 @@ + + * + * 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 +{ + /** + * 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..6722f7f03fc --- /dev/null +++ b/htdocs/attributes/class/ProductCombination.class.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 . + */ + +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..93521b86bbf --- /dev/null +++ b/htdocs/attributes/class/ProductCombination2ValuePair.class.php @@ -0,0 +1,151 @@ + + * + * 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 +{ + /** + * 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 @@ -48,6 +49,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'); @@ -655,7 +660,8 @@ if (empty($reshook)) $predef=''; $product_desc=(GETPOST('dp_desc')?GETPOST('dp_desc'):''); $price_ht = GETPOST('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); @@ -681,21 +687,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 == '') // Unit price can be 0 but not ''. Also price can be negative for proposal. + if ($prod_entry_mode == 'free' && empty($idprod) && $price_ht == '') // 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; diff --git a/htdocs/commande/card.php b/htdocs/commande/card.php index d91ee35fe3e..20def0175ba 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'); @@ -621,7 +625,8 @@ if (empty($reshook)) $predef=''; $product_desc=(GETPOST('dp_desc')?GETPOST('dp_desc'):''); $price_ht = GETPOST('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); @@ -651,11 +656,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 == '')) // Unit price can be 0 but not '' + if ($prod_entry_mode == 'free' && empty($idprod) && (! ($price_ht >= 0) || $price_ht == '')) // Unit price can be 0 but not '' { setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("UnitPriceHT")), null, 'errors'); $error++; @@ -664,11 +669,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')); diff --git a/htdocs/compta/facture.php b/htdocs/compta/facture.php index b2b39e375d7..be38e4b48f1 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'); @@ -1322,7 +1326,8 @@ if (empty($reshook)) $predef=''; $product_desc=(GETPOST('dp_desc')?GETPOST('dp_desc'):''); $price_ht = GETPOST('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); @@ -1352,11 +1357,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 == '')) // Unit price can be 0 but not '' + if ($prod_entry_mode == 'free' && empty($idprod) && (! ($price_ht >= 0) || $price_ht == '')) // Unit price can be 0 but not '' { setEventMessages($langs->trans("ErrorFieldRequired", $langs->transnoentitiesnoconv("UnitPriceHT")), null, 'errors'); $error ++; @@ -1365,7 +1370,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 ++; } @@ -1374,7 +1379,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); diff --git a/htdocs/contrat/card.php b/htdocs/contrat/card.php index 5a87d3b3d51..4a20f1b8f9e 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 @@ -471,7 +471,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 61953629166..ba60aa5f2c5 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 @@ -1605,9 +1605,10 @@ class Form * @param int $hidelabel Hide label (0=no, 1=yes, 2=show search icon (before) and placeholder, 3 search icon after) * @param array $ajaxoptions Options for ajax_autocompleter * @param int $socid Thirdparty Id (to get also price dedicated to this customer) + * @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) + 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, $selected_combinations = array()) { global $langs,$conf; @@ -1632,6 +1633,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,17 @@ 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 (!empty($conf->global->PRODUIT_ATTRIBUTES_HIDECHILD)) { + $sql .= " AND pac.rowid IS NULL"; + } + if ($finished == 0) { $sql.= " AND p.finished = ".$finished; @@ -1881,7 +1966,7 @@ class Form $outlabel=$objp->label; $outdesc=$objp->description; $outbarcode=$objp->barcode; - + $outtype=$objp->fk_product_type; $outdurationvalue=$outtype == Product::TYPE_SERVICE?substr($objp->duration,0,dol_strlen($objp->duration)-1):''; $outdurationunit=$outtype == Product::TYPE_SERVICE?substr($objp->duration,-1):''; @@ -1898,13 +1983,13 @@ class Form $opt.= $objp->ref; if ($outbarcode) $opt.=' ('.$outbarcode.')'; $opt.=' - '.dol_trunc($label,$maxlengtharticle).' - '; - + $objRef = $objp->ref; if (! empty($filterkey) && $filterkey != '') $objRef=preg_replace('/('.preg_quote($filterkey).')/i','$1',$objRef,1); $outval.=$objRef; if ($outbarcode) $outval.=' ('.$outbarcode.')'; $outval.=' - '.dol_trunc($label,$maxlengtharticle).' - '; - + $found=0; // Multiprice @@ -2066,7 +2151,7 @@ class Form { global $langs,$conf; global $price_level, $status, $finished; - + $selected_input_value=''; if (! empty($conf->use_javascript_ajax) && ! empty($conf->global->PRODUIT_USE_SEARCH_TO_SELECT)) { @@ -3174,7 +3259,7 @@ class Form { dol_syslog(__METHOD__ . ': using numeric value for parameter type is deprecated. Use string code instead.', LOG_WARNING); } - + $cat = new Categorie($this->db); $cate_arbo = $cat->get_full_arbo($type,$excludeafterid); @@ -3837,7 +3922,7 @@ class Form function form_multicurrency_rate($page, $rate='', $htmlname='multicurrency_tx', $currency='') { global $langs, $mysoc, $conf; - + if ($htmlname != "none") { print '
'; @@ -4107,7 +4192,7 @@ class Form $out.= ''; } } - + } $out.= ''; @@ -4204,7 +4289,7 @@ class Form $defaulttx=preg_replace('/\s*\(.*\)/','',$defaulttx); } //var_dump($selectedrate.'-'.$defaulttx.'-'.$defaultnpr.'-'.$defaultcode); - + // Check parameters if (is_object($societe_vendeuse) && ! $societe_vendeuse->country_code) { @@ -4263,7 +4348,7 @@ class Form // Now we get list $num = $this->load_cache_vatrates($code_country); // If no vat defined, return -1 with message into this->error - + if ($num > 0) { // Definition du taux a pre-selectionner (si defaulttx non force et donc vaut -1 ou '') @@ -4304,7 +4389,7 @@ class Form $return.= '"'; if ($defaultcode) // If defaultcode is defined, we used it in priority to select combo option instead of using rate+npr flag { - if ($defaultcode == $rate['code']) $return.= ' selected'; + if ($defaultcode == $rate['code']) $return.= ' selected'; } elseif ($rate['txtva'] == $defaulttx && $rate['nprtva'] == $defaultnpr) { @@ -5036,7 +5121,7 @@ class Form /* var_dump($val); var_dump(array_key_exists('enabled', $val)); var_dump(!$val['enabled']);*/ - if (array_key_exists('enabled', $val) && isset($val['enabled']) && ! $val['enabled']) + if (array_key_exists('enabled', $val) && isset($val['enabled']) && ! $val['enabled']) { unset($array[$key]); // We don't want this field continue; @@ -5221,17 +5306,17 @@ class Form else if ($objecttype == 'subscription') { $tplpath = 'adherents'; } - + global $linkedObjectBlock; $linkedObjectBlock = $objects; if (empty($numoutput)) { $numoutput++; - + print '
'; print load_fiche_titre($langs->trans('RelatedObjects'), '', ''); - + print ''; print ''; @@ -5244,7 +5329,7 @@ class Form print ''; print ''; } - + // Output template part (modules that overwrite templates must declare this into descriptor) $dirtpls=array_merge($conf->modules_parts['tpl'],array('/'.$tplpath.'/tpl')); foreach($dirtpls as $reldir) @@ -5254,11 +5339,11 @@ class Form } } - if ($numoutput) + if ($numoutput) { print '
'; } - + return $num; } } @@ -5627,7 +5712,7 @@ class Form $ret.=''; if ($morehtmlright) $ret.='
'.$morehtmlright.'
'; - + if ($previous_ref || $next_ref || $morehtml) { $ret.=''; print '
'; diff --git a/htdocs/product/class/product.class.php b/htdocs/product/class/product.class.php index f3f03c65ac6..e87b00ee949 100644 --- a/htdocs/product/class/product.class.php +++ b/htdocs/product/class/product.class.php @@ -839,6 +839,17 @@ class Product extends CommonObject if (! $error) { + if ($conf->attributes->enabled) { + + require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductCombination.class.php'; + + $comb = new ProductCombination($this->db); + + foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) { + $currcomb->updateProperties($this); + } + } + $this->db->commit(); return 1; } @@ -944,6 +955,25 @@ class Product extends CommonObject } } + if (!$error) { + + require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductCombination.class.php'; + + //If it is a parent product, then we remove the association with child products + $prodcomb = new ProductCombination($this->db); + + if ($prodcomb->deleteByFkProductParent($id) < 0) { + $error++; + $this->errors[] = 'Error deleting combinations'; + } + + //We also check if it is a child product + if (!$error && ($prodcomb->fetchByFkProductChild($id) > 0) && ($prodcomb->delete() < 0)) { + $error++; + $this->errors[] = 'Error deleting child combination'; + } + } + // Delete product if (! $error) { diff --git a/htdocs/product/list.php b/htdocs/product/list.php index c95156b4393..29e2216c154 100644 --- a/htdocs/product/list.php +++ b/htdocs/product/list.php @@ -2,7 +2,7 @@ /* Copyright (C) 2001-2006 Rodolphe Quiedeville * Copyright (C) 2004-2016 Laurent Destailleur * Copyright (C) 2005-2012 Regis Houssin - * Copyright (C) 2012-2013 Marcos García + * Copyright (C) 2012-2016 Marcos García * Copyright (C) 2013-2016 Juanjo Menent * Copyright (C) 2013-2015 Raphaël Doursenaud * Copyright (C) 2013 Jean Heimburger @@ -50,7 +50,7 @@ $sref=GETPOST("sref"); $sbarcode=GETPOST("sbarcode"); $snom=GETPOST("snom"); $sall=GETPOST("sall"); -$type=GETPOST("type","int"); +$type= (int) GETPOST("type","int"); $search_sale = GETPOST("search_sale"); $search_categ = GETPOST("search_categ",'int'); $tosell = GETPOST("tosell", 'int'); @@ -62,6 +62,13 @@ $search_accountancy_code_sell = GETPOST("search_accountancy_code_sell",'alpha'); $search_accountancy_code_buy = GETPOST("search_accountancy_code_buy",'alpha'); $optioncss = GETPOST('optioncss','alpha'); +//Show/hide child products. Hidden by default +if (!$_POST) { + $search_hidechildproducts = 'on'; +} else { + $search_hidechildproducts = GETPOST('search_hidechildproducts'); +} + $limit = GETPOST("limit")?GETPOST("limit","int"):$conf->liste_limit; $sortfield = GETPOST("sortfield",'alpha'); $sortorder = GETPOST("sortorder",'alpha'); @@ -170,7 +177,7 @@ if (is_array($extrafields->attribute_label) && count($extrafields->attribute_lab } - + /* * Actions */ @@ -230,6 +237,9 @@ else $sql.= ' p.datec as date_creation, p.tms as date_update,'; //$sql.= ' pfp.ref_fourn as ref_supplier, '; $sql.= ' MIN(pfp.unitprice) as minsellprice'; + if (!empty($conf->attributes->enabled) && $search_hidechildproducts && ($type === 0)) { + $sql .= ', pac.rowid prod_comb_id'; + } // Add fields from extrafields foreach ($extrafields->attribute_label as $key => $val) $sql.=($extrafields->attribute_type[$key] != 'separate' ? ",ef.".$key.' as options_'.$key : ''); // Add fields from hooks @@ -242,6 +252,10 @@ else $sql.= " LEFT JOIN ".MAIN_DB_PREFIX."product_fournisseur_price as pfp ON p.rowid = pfp.fk_product"; // multilang if (! empty($conf->global->MAIN_MULTILANGS)) $sql.= " LEFT JOIN ".MAIN_DB_PREFIX."product_lang as pl ON pl.fk_product = p.rowid AND pl.lang = '".$langs->getDefaultLang() ."'"; + if (!empty($conf->attributes->enabled) && $search_hidechildproducts && ($type === 0)) { + $sql .= " LEFT JOIN ".MAIN_DB_PREFIX."product_attribute_combination pac ON pac.fk_product_child = p.rowid"; + } + $sql.= ' WHERE p.entity IN ('.getEntity('product', 1).')'; if ($sall) $sql .= natural_search(array_keys($fieldstosearchall), $sall); // if the type is not 1, we show all products (type = 0,2,3) @@ -265,6 +279,12 @@ else if ($search_accountancy_code_sell) $sql.= natural_search('p.accountancy_code_sell', $search_accountancy_code_sell); if ($search_accountancy_code_sell) $sql.= natural_search('p.accountancy_code_buy', $search_accountancy_code_buy); // Add where from extra fields + + if (!empty($conf->attributes->enabled) && $search_hidechildproducts && ($type === 0)) { + $sql .= " AND pac.rowid IS NULL"; + } + + // Add where from extra fields foreach ($search_array_options as $key => $val) { $crit=$val; @@ -284,6 +304,9 @@ else $sql.= " GROUP BY p.rowid, p.ref, p.label, p.barcode, p.price, p.price_ttc, p.price_base_type,"; $sql.= " p.fk_product_type, p.duration, p.tosell, p.tobuy, p.seuil_stock_alerte, p.desiredstock,"; $sql.= ' p.datec, p.tms, p.entity, p.tobatch, p.accountancy_code_sell, p.accountancy_code_buy'; + if (!empty($conf->attributes->enabled) && $search_hidechildproducts && ($type === 0)) { + $sql .= ', pac.rowid'; + } // Add fields from extrafields foreach ($extrafields->attribute_label as $key => $val) $sql.=($extrafields->attribute_type[$key] != 'separate' ? ",ef.".$key : ''); // Add fields from hooks @@ -415,6 +438,15 @@ else $moreforfilter.=$htmlother->select_categories(Categorie::TYPE_PRODUCT,$search_categ,'search_categ',1); $moreforfilter.='
'; } + + //Show/hide child products. Hidden by default + if (!empty($conf->attributes->enabled) && $type === 0) { + $moreforfilter.='
'; + $moreforfilter.= ''; + $moreforfilter.= ' '; + $moreforfilter.='
'; + } + if ($moreforfilter) { print '
'; @@ -614,7 +646,7 @@ else $product_static->status_buy = $objp->tobuy; $product_static->status = $objp->tosell; $product_static->entity = $objp->entity; - + if (! empty($conf->stock->enabled) && $user->rights->stock->lire && $type != 1) // To optimize call of load_stock { if ($objp->fk_product_type != 1) // Not a service @@ -622,8 +654,8 @@ else $product_static->load_stock('nobatch'); // Load stock_reel + stock_warehouse. This also call load_virtual_stock() } } - - + + $var=!$var; print ''; @@ -752,7 +784,7 @@ else print ''; print yn($objp->tobatch); print ''; - } + } // Accountancy code sell if (! empty($arrayfields['p.accountancy_code_sell']['checked'])) print ''.$objp->accountancy_code_sell.''; // Accountancy code sell From 8ceb494569c5d30eecc8f8b13265ab08fa4029c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Garci=CC=81a=20de=20La=20Fuente?= Date: Mon, 25 Jul 2016 10:37:39 +0200 Subject: [PATCH 2/5] Added comments on classes to fix travis errors --- htdocs/attributes/class/ProductAttribute.class.php | 4 ++++ htdocs/attributes/class/ProductAttributeValue.class.php | 4 ++++ htdocs/attributes/class/ProductCombination.class.php | 4 ++++ .../attributes/class/ProductCombination2ValuePair.class.php | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/htdocs/attributes/class/ProductAttribute.class.php b/htdocs/attributes/class/ProductAttribute.class.php index 1e0837fcb97..f563b03cd7e 100644 --- a/htdocs/attributes/class/ProductAttribute.class.php +++ b/htdocs/attributes/class/ProductAttribute.class.php @@ -16,6 +16,10 @@ * along with this program. If not, see . */ +/** + * Class ProductAttribute + * Used to represent a product attribute + */ class ProductAttribute { /** diff --git a/htdocs/attributes/class/ProductAttributeValue.class.php b/htdocs/attributes/class/ProductAttributeValue.class.php index f500fb251c6..41d1818ee63 100644 --- a/htdocs/attributes/class/ProductAttributeValue.class.php +++ b/htdocs/attributes/class/ProductAttributeValue.class.php @@ -16,6 +16,10 @@ * along with this program. If not, see . */ +/** + * Class ProductAttributeValue + * Used to represent a product attribute value + */ class ProductAttributeValue { /** diff --git a/htdocs/attributes/class/ProductCombination.class.php b/htdocs/attributes/class/ProductCombination.class.php index 6722f7f03fc..d5d7b6e81af 100644 --- a/htdocs/attributes/class/ProductCombination.class.php +++ b/htdocs/attributes/class/ProductCombination.class.php @@ -16,6 +16,10 @@ * along with this program. If not, see . */ +/** + * Class ProductCombination + * Used to represent a product combination + */ class ProductCombination { /** diff --git a/htdocs/attributes/class/ProductCombination2ValuePair.class.php b/htdocs/attributes/class/ProductCombination2ValuePair.class.php index 93521b86bbf..01bd0952d2d 100644 --- a/htdocs/attributes/class/ProductCombination2ValuePair.class.php +++ b/htdocs/attributes/class/ProductCombination2ValuePair.class.php @@ -16,6 +16,10 @@ * 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 { /** From 1c15cfa7229ee79e47405c670cbab111bb770c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Garci=CC=81a=20de=20La=20Fuente?= Date: Sun, 6 Nov 2016 15:17:41 +0100 Subject: [PATCH 3/5] Adapted code to new changes --- htdocs/core/tpl/objectline_create.tpl.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htdocs/core/tpl/objectline_create.tpl.php b/htdocs/core/tpl/objectline_create.tpl.php index 4f0c1e4f479..5a82d1e8c26 100644 --- a/htdocs/core/tpl/objectline_create.tpl.php +++ b/htdocs/core/tpl/objectline_create.tpl.php @@ -185,7 +185,7 @@ else { if (empty($senderissupplier)) { - $form->select_produits(GETPOST('idprod'), 'idprod', $filtertype, $conf->product->limit_size, $buyer->price_level, 1, 2, '', 1, array(),$buyer->id, GETPOST('combinations', 'array')); + $form->select_produits(GETPOST('idprod'), 'idprod', $filtertype, $conf->product->limit_size, $buyer->price_level, 1, 2, '', 1, array(), $buyer->id, 1, 0, '', 0, GETPOST('combinations', 'array')); } else { From 91be05932e4ef150e2d525c598fb503698c019fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Garci=CC=81a=20de=20La=20Fuente?= Date: Sun, 27 Nov 2016 21:50:27 +0100 Subject: [PATCH 4/5] Added missing modAttributes file --- htdocs/core/modules/modAttributes.class.php | 137 ++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 htdocs/core/modules/modAttributes.class.php diff --git a/htdocs/core/modules/modAttributes.class.php b/htdocs/core/modules/modAttributes.class.php new file mode 100644 index 00000000000..a21ae756bf2 --- /dev/null +++ b/htdocs/core/modules/modAttributes.class.php @@ -0,0 +1,137 @@ + + * Copyright (C) 2004-2012 Laurent Destailleur + * Copyright (C) 2005-2012 Regis Houssin + * 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 + * 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 . + */ + +/** + * \defgroup produit Module product attributes + * \brief Module to manage product combinations based on product attributes + * \file htdocs/core/modules/modAttributes.class.php + * \ingroup produit + * \brief File to describe module to manage catalog of predefined products + */ +include_once DOL_DOCUMENT_ROOT .'/core/modules/DolibarrModules.class.php'; + + +/** + * Description and activation class for module Product attributes + */ +class modAttributes extends DolibarrModules +{ + /** + * Constructor. Define names, constants, directories, boxes, permissions + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + global $langs,$conf; + + $this->db = $db; + + // Id for module (must be unique). + // Use here a free id (See in Home -> System information -> Dolibarr for list of used modules id). + $this->numero = 610; + // Key text used to identify module (for permissions, menus, etc...) + $this->rights_class = 'attributes'; + + // Family can be 'crm','financial','hr','projects','products','ecm','technic','other' + // It is used to group modules in module setup page + $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)); + // Module description, used if translation string 'ModuleXXXDesc' not found (where XXX is value of numeric property 'numero' of module) + $this->description = 'Allows creating product combinations based on product attributes'; + // Possible values for version are: 'development', 'experimental', 'dolibarr' or version + $this->version = 'dolibarr'; + // 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. + // If file is in theme/yourtheme/img directory under name object_pictovalue.png, use this->picto='pictovalue' + // If file is in module/img directory under name object_pictovalue.png, use this->picto='pictovalue@module' + $this->picto='product'; + + // Defined all module parts (triggers, login, substitutions, menus, css, etc...) + $this->module_parts = array(); + + // Data directories to create when module is enabled. + // Example: this->dirs = array("/mymodule/temp"); + $this->dirs = array(); + + // Config pages. Put here list of php page, stored into mymodule/admin directory, to use to setup module. + $this->config_page_url = array( + 'admin.php@attributes' + ); + + // Dependencies + $this->hidden = false; // A condition to hide module + $this->depends = array( + 'modProduct' + ); // List of modules id that must be enabled if this module is enabled + $this->requiredby = array(); // List of modules id to disable if this one is disabled + $this->conflictwith = array(); // List of modules id this module is in conflict with + $this->phpmin = array(5,0); // Minimum version of PHP required by module + $this->need_dolibarr_version = array(3,0); // Minimum version of Dolibarr required by module + $this->langfiles = array("products"); + + // Constants + $this->const = array(); + + // Array to add new pages in new tabs + $this->tabs = array( +// 'product:+combinations:Combinaciones:products:1:/attributes/combinations.php?id=__ID__' + ); + + // Dictionaries + if (! isset($conf->mymodule->enabled)) + { + $conf->mymodule=new stdClass(); + $conf->mymodule->enabled=0; + } + $this->dictionaries=array(); + + // Boxes + // Add here list of php file(s) stored in core/boxes that contains class to show a box. + $this->boxes = array(); // List of boxes + + // Permissions + $this->rights = array(); // Permission array used by this module + + // Main menu entries + $this->menu = array( + array( + 'fk_menu' => 'fk_mainmenu=products,fk_leftmenu=product', + 'type' => 'left', + 'titre' => $langs->trans('Attributes'), + 'mainmenu' => 'products', + 'leftmenu' => 'product', + 'url' => '/attributes/list.php', + 'langs' => 'products', + 'position' => 100, + 'enabled' => '$conf->product->enabled', + 'perms' => 1, + 'target' => '', + 'user' => 0 + ) + ); // List of menus to add + } +} + From 154425e2bcca8ca241ebf6d6ae7b53eeaa027803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Garci=CC=81a=20de=20La=20Fuente?= Date: Tue, 27 Dec 2016 16:13:39 +0100 Subject: [PATCH 5/5] Fixed PHP fatal error --- htdocs/product/class/product.class.php | 1 + 1 file changed, 1 insertion(+) diff --git a/htdocs/product/class/product.class.php b/htdocs/product/class/product.class.php index 418b66d4ac7..2f5540cbc8b 100644 --- a/htdocs/product/class/product.class.php +++ b/htdocs/product/class/product.class.php @@ -976,6 +976,7 @@ class Product extends CommonObject if (!$error) { require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductCombination.class.php'; + require_once DOL_DOCUMENT_ROOT.'/attributes/class/ProductCombination2ValuePair.class.php'; //If it is a parent product, then we remove the association with child products $prodcomb = new ProductCombination($this->db);