浏览代码

Merge branch 'develop' into patch-8

Laurent Destailleur 4 年之前
父节点
当前提交
0d59ac34d2
共有 59 个文件被更改,包括 1256 次插入405 次删除
  1. 18 14
      README.md
  2. 1 1
      build/debian/control
  3. 6 3
      htdocs/accountancy/admin/fiscalyear.php
  4. 1 1
      htdocs/admin/modules.php
  5. 5 3
      htdocs/bom/bom_card.php
  6. 1 1
      htdocs/bom/tpl/objectline_create.tpl.php
  7. 3 1
      htdocs/compta/facture/list.php
  8. 8 3
      htdocs/compta/facture/prelevement.php
  9. 10 2
      htdocs/compta/index.php
  10. 7 1
      htdocs/contact/list.php
  11. 6 6
      htdocs/core/class/CMailFile.class.php
  12. 75 0
      htdocs/core/class/fiscalyear.class.php
  13. 6 2
      htdocs/core/class/html.form.class.php
  14. 14 13
      htdocs/core/lib/ajax.lib.php
  15. 2 1
      htdocs/core/lib/functions.lib.php
  16. 4 7
      htdocs/core/lib/takepos.lib.php
  17. 1 1
      htdocs/core/menus/standard/eldy.lib.php
  18. 1 1
      htdocs/core/tpl/admin_extrafields_add.tpl.php
  19. 1 1
      htdocs/core/tpl/admin_extrafields_edit.tpl.php
  20. 1 1
      htdocs/core/tpl/advtarget.tpl.php
  21. 1 1
      htdocs/core/tpl/ajax/fileupload_main.tpl.php
  22. 1 1
      htdocs/core/tpl/ajax/objectlinked_lineimport.tpl.php
  23. 1 1
      htdocs/core/tpl/bloc_showhide.tpl.php
  24. 1 1
      htdocs/core/tpl/extrafields_view.tpl.php
  25. 1 1
      htdocs/core/tpl/filemanager.tpl.php
  26. 5 6
      htdocs/core/tpl/login.tpl.php
  27. 50 3
      htdocs/core/tpl/objectline_create.tpl.php
  28. 2 2
      htdocs/core/tpl/objectline_edit.tpl.php
  29. 1 1
      htdocs/core/tpl/onlinepaymentlinks.tpl.php
  30. 5 6
      htdocs/core/tpl/passwordforgotten.tpl.php
  31. 2 2
      htdocs/holiday/card.php
  32. 17 0
      htdocs/install/mysql/migration/12.0.0-13.0.0.sql
  33. 2 2
      htdocs/install/mysql/tables/llx_product.sql
  34. 28 0
      htdocs/install/mysql/tables/llx_product_attribute_combination_price_level.sql
  35. 2 2
      htdocs/install/mysql/tables/llx_product_stock_entrepot.sql
  36. 4 4
      htdocs/langs/en_US/cashdesk.lang
  37. 2 2
      htdocs/langs/en_US/main.lang
  38. 4 0
      htdocs/langs/en_US/products.lang
  39. 3 3
      htdocs/langs/en_US/projects.lang
  40. 2 2
      htdocs/langs/fr_FR/main.lang
  41. 2 1
      htdocs/modulebuilder/template/myobject_list.php
  42. 1 1
      htdocs/product/card.php
  43. 118 104
      htdocs/product/class/api_products.class.php
  44. 4 4
      htdocs/product/class/product.class.php
  45. 1 1
      htdocs/product/composition/card.php
  46. 6 2
      htdocs/product/stock/list.php
  47. 1 1
      htdocs/projet/info.php
  48. 2 4
      htdocs/public/demo/index.php
  49. 128 113
      htdocs/takepos/admin/bar.php
  50. 13 0
      htdocs/takepos/admin/receipt.php
  51. 0 16
      htdocs/takepos/admin/setup.php
  52. 1 0
      htdocs/takepos/invoice.php
  53. 7 2
      htdocs/theme/eldy/info-box.inc.php
  54. 1 1
      htdocs/theme/eldy/theme_vars.inc.php
  55. 3 3
      htdocs/variants/card.php
  56. 42 10
      htdocs/variants/class/ProductAttribute.class.php
  57. 76 25
      htdocs/variants/class/ProductAttributeValue.class.php
  58. 456 8
      htdocs/variants/class/ProductCombination.class.php
  59. 89 6
      htdocs/variants/combinations.php

+ 18 - 14
README.md

@@ -3,13 +3,13 @@
 ![Downloads per day](https://img.shields.io/sourceforge/dw/dolibarr.svg)
 ![Build status](https://img.shields.io/travis/Dolibarr/dolibarr/develop.svg)
 
-Dolibarr ERP & CRM is a modern software package to manage your organization's activity (contacts, suppliers, invoices, orders, stocks, agenda…).
+Dolibarr ERP & CRM is a modern software package that helps manage your organization's activity (contacts, suppliers, invoices, orders, stocks, agenda…).
 
-It's an Open Source Software (written in PHP with optional JavaScript enhancements) designed for small, medium or large companies, foundations and freelancers.
+It's an Open Source Software suite (written in PHP with optional JavaScript enhancements) designed for small, medium or large companies, foundations and freelancers.
 
-You can freely use, study, modify or distribute it according to its Free Software licence.
+You can freely use, study, modify or distribute it according to its licence.
 
-You can use it as a standalone application or as a web application to be able to access it from the Internet or a LAN.
+You can use it as a standalone application or as a web application to access it from the Internet or a LAN.
 
 ![ScreenShot](https://www.dolibarr.org/images/dolibarr_screenshot1_1920x1080.jpg)
 
@@ -35,11 +35,15 @@ Releases can be downloaded from [official website](https://www.dolibarr.org/).
 
 ### Advanced setup
 
-You can use a Web server and a supported database (MariaDB, MySQL or PostgreSQL) to install the standard version.
+You can use a web server and a supported database (MariaDB, MySQL or PostgreSQL) to install the standard version.
+
+On GNU/Linux, first check if your distribution has already packaged Dolibarr.
+
+#### Generic install steps:
 
 - Check that your installed PHP version is supported [see PHP support](https://wiki.dolibarr.org/index.php/Versions).
 
-- Uncompress the downloaded .zip archive to copy the "dolibarr/htdocs" directory and all its files inside your web server root or get the files directly from GitHub (recommanded if you known git):
+- Uncompress the downloaded .zip archive to copy the "dolibarr/htdocs" directory and all its files inside your web server root or get the files directly from GitHub (recommanded if you know git as it makes it easier if you want to upgrade later):
 
   `git clone https://github.com/dolibarr/dolibarr -b x.y`     (where x.y is main version like 3.6, 9.0, ...)
 
@@ -70,11 +74,11 @@ If you don't have time to install it yourself, you can try some commercial 'read
 
 ## UPGRADING
 
-- At first make a backup of your Dolibarr files & than see https://wiki.dolibarr.org/index.php/Installation_-_Upgrade#Upgrade_Dolibarr
+- At first make a backup of your Dolibarr files & then read https://wiki.dolibarr.org/index.php/Installation_-_Upgrade#Upgrade_Dolibarr
 - Check that your installed PHP version is supported by the new version [see PHP support](./doc/phpmatrix.md).
 - Overwrite all old files from 'dolibarr' directory with files provided into the new version's package.
-- At first next access, Dolibarr will redirect your to the "install/" page to follow the upgrade process.
-  If an `install.lock` file exists to lock any other upgrade process, the application will ask you to remove the file manually (you should find the `install.lock` file into the directory used to store generated and uploaded documents, in most cases, it is the directory called "*documents*").
+- At first next access, Dolibarr will redirect you to the "install/" page to follow the upgrade process.
+  If an `install.lock` file exists to lock any other upgrade process, the application will ask you to remove the file manually (you should find the `install.lock` file in the directory used to store generated and uploaded documents, in most cases, it is the directory called "*documents*").
 
 *Note: migration process can be safely done multiple times by calling the `/install/index.php` page*
 
@@ -139,7 +143,7 @@ See the [ChangeLog](https://github.com/Dolibarr/dolibarr/blob/develop/ChangeLog)
 - Highly customizable: enable only the modules you need, add user personalized fields, choose your skin, several menu managers (can be used by internal users as a back-office with a particular menu, or by external users as a front-office with another one)
 
 - APIs
-- An easy to understand, maintain and develop code (PHP with no heavy framework; trigger and hook architecture)
+- Code that is easy to understand, maintain and develop (PHP with no heavy framework; trigger and hook architecture)
 - Support a lot of country specific features:
   - Spanish Tax RE and ISPF
   - French NPR VAT rate (VAT called "Non Perçue Récupérable" for DOM-TOM)
@@ -149,7 +153,7 @@ See the [ChangeLog](https://github.com/Dolibarr/dolibarr/blob/develop/ChangeLog)
   - Compatible with [European directives](http://europa.eu/legislation_summaries/taxation/l31057_en.htm) (2006/112/CE ... 2010/45/UE)
   - Compatible with European GDPR rules
   - ...
-- PDF or ODT generation for invoice, proposals, orders...
+- Flexible PDF & ODT generation for invoices, proposals, orders...
 - …
 
 ### System Environment / Requirements
@@ -167,12 +171,12 @@ These are features that Dolibarr does **not** yet fully support:
 
 - Tasks dependencies in projects
 - Payroll module
-- No native embedded Webmail
+- No native embedded Webmail, but you can send email to contacts in Dolibarr with e.g. offers, invoices, etc.
 - Dolibarr can't do coffee (yet)
 
 ## DOCUMENTATION
 
-Administrator, user, developer and translator's documentations are available along with other community resources on the [Wiki](https://wiki.dolibarr.org).
+Administrator, user, developer and translator's documentations are available along with other community resources in the [Wiki](https://wiki.dolibarr.org).
 
 ## CONTRIBUTING
 
@@ -182,7 +186,7 @@ This project exists thanks to all the people who contribute. [[Contribute](https
 
 ## CREDITS
 
-Dolibarr is the work of many contributors over the years and uses some fine libraries.
+Dolibarr is the work of many contributors over the years and uses some fine PHP libraries.
 
 See [COPYRIGHT](https://github.com/Dolibarr/dolibarr/blob/develop/COPYRIGHT) file.
 

+ 1 - 1
build/debian/control

@@ -14,7 +14,7 @@ Architecture: all
 Depends: libapache2-mod-php5 | libapache2-mod-php5filter | php5-cgi | php5-fpm | php5 | libapache2-mod-php | libapache2-mod-phpfilter | php-cgi | php-fpm | php,
     php5-cli | php-cli,
 # Required PHP extensions
-    php5-mysql | php5-mysqli | php-mysql | php-mysqli, php5-curl | php-curl, php5-gd | php-gd, php5-ldap | php-gd,
+    php5-mysql | php5-mysqli | php-mysql | php-mysqli, php5-curl | php-curl, php5-gd | php-gd, php5-ldap | php-gd, php5-zip | php-zip,
 # Required PHP libraries
     php-pear, php-mail-mime,
 #    php-tcpdf,

+ 6 - 3
htdocs/accountancy/admin/fiscalyear.php

@@ -79,6 +79,7 @@ $object = new Fiscalyear($db);
 $max = 100;
 
 $form = new Form($db);
+$fiscalyearstatic = new Fiscalyear($db);
 
 $title = $langs->trans('AccountingPeriods');
 $helpurl = "";
@@ -132,13 +133,15 @@ if ($result)
 	print '</tr>';
 
 	if ($num) {
-		$fiscalyearstatic = new Fiscalyear($db);
-
 		while ($i < $num && $i < $max) {
 			$obj = $db->fetch_object($result);
+
 			$fiscalyearstatic->id = $obj->rowid;
+
 			print '<tr class="oddeven">';
-			print '<td><a href="fiscalyear_card.php?id='.$obj->rowid.'">'.img_object($langs->trans("ShowFiscalYear"), "technic").' '.$obj->rowid.'</a></td>';
+			print '<td>';
+			print $fiscalyearstatic->getNomUrl(1);
+			print '</td>';
 			print '<td class="left">'.$obj->label.'</td>';
 			print '<td class="left">'.dol_print_date($db->jdate($obj->date_start), 'day').'</td>';
 			print '<td class="left">'.dol_print_date($db->jdate($obj->date_end), 'day').'</td>';

+ 1 - 1
htdocs/admin/modules.php

@@ -38,7 +38,7 @@ require_once DOL_DOCUMENT_ROOT.'/admin/dolistore/class/dolistore.class.php';
 // Load translation files required by the page
 $langs->loadLangs(array("errors", "admin", "modulebuilder"));
 
-$mode = GETPOSTISSET('mode') ? GETPOST('mode', 'alpha') : 'commonkanban';
+$mode = GETPOSTISSET('mode') ? GETPOST('mode', 'alpha') : (empty($conf->global->MAIN_MODULE_SETUP_ON_LIST_BY_DEFAULT) ? 'commonkanban' : 'common');
 if (empty($mode)) $mode = 'common';
 $action = GETPOST('action', 'alpha');
 //var_dump($_POST);exit;

+ 5 - 3
htdocs/bom/bom_card.php

@@ -192,10 +192,10 @@ if (empty($reshook))
 		$error = 0;
 
 		// Set if we used free entry or predefined product
-		$qty = GETPOST('qty', 'int');
+		$qty = price2num(GETPOST('qty', 'int'));
 		$qty_frozen = GETPOST('qty_frozen', 'int');
 		$disable_stock_change = GETPOST('disable_stock_change', 'int');
-		$efficiency = GETPOST('efficiency', 'int');
+		$efficiency = price2num(GETPOST('efficiency', 'int'));
 
 		if ($qty == '') {
 			setEventMessages($langs->trans('ErrorFieldRequired', $langs->transnoentitiesnoconv('Qty')), null, 'errors');
@@ -219,6 +219,8 @@ if (empty($reshook))
 			unset($_POST['qty']);
 			unset($_POST['qty_frozen']);
 		    unset($_POST['disable_stock_change']);
+
+		    $object->fetchLines();
 		}
 	}
 }
@@ -541,7 +543,7 @@ if ($object->id > 0 && (empty($action) || ($action != 'edit' && $action != 'crea
 
 	if (!empty($object->table_element_line))
 	{
-	    print '	<form name="addproduct" id="addproduct" action="'.$_SERVER["PHP_SELF"].'?id='.$object->id.(($action != 'editline') ? '#addline' : '#line_'.GETPOST('lineid', 'int')).'" method="POST">
+	    print '	<form name="addproduct" id="addproduct" action="'.$_SERVER["PHP_SELF"].'?id='.$object->id.(($action != 'editline') ? '#addline' : '').'" method="POST">
     	<input type="hidden" name="token" value="' . newToken().'">
     	<input type="hidden" name="action" value="' . (($action != 'editline') ? 'addline' : 'updateline').'">
     	<input type="hidden" name="mode" value="">

+ 1 - 1
htdocs/bom/tpl/objectline_create.tpl.php

@@ -113,7 +113,7 @@ if ($conf->global->PRODUCT_USE_UNITS)
 {
     $coldisplay++;
 	print '<td class="nobottom linecoluseunit left">';
-	print $form->selectUnits($line->fk_unit, "units");
+	print $form->selectUnits(empty($line->fk_unit) ? $conf->global->PRODUCT_USE_UNITS : $line->fk_unit, "units");
 	print '</td>';
 }
 

+ 3 - 1
htdocs/compta/facture/list.php

@@ -5,7 +5,7 @@
  * Copyright (C) 2005      Marc Barilley / Ocebo <marc@ocebo.com>
  * Copyright (C) 2005-2015 Regis Houssin         <regis.houssin@inodbox.com>
  * Copyright (C) 2006      Andre Cianfarani      <acianfa@free.fr>
- * Copyright (C) 2010-2012 Juanjo Menent         <jmenent@2byte.es>
+ * Copyright (C) 2010-2020 Juanjo Menent         <jmenent@2byte.es>
  * Copyright (C) 2012      Christophe Battarel   <christophe.battarel@altairis.fr>
  * Copyright (C) 2013      Florian Henry         <florian.henry@open-concept.pro>
  * Copyright (C) 2013      Cédric Salvador       <csalvador@gpcsolutions.fr>
@@ -545,6 +545,8 @@ if (!$sall)
 	$sql .= ' f.paye, f.fk_statut, f.close_code,';
 	$sql .= ' f.datec, f.tms, f.date_closing,';
 	$sql .= ' f.retained_warranty, f.retained_warranty_date_limit, f.situation_final, f.situation_cycle_ref, f.situation_counter,';
+	$sql .= ' f.fk_user_author, f.fk_multicurrency, f.multicurrency_code, f.multicurrency_tx, f.multicurrency_total_ht, f.multicurrency_total_tva,';
+	$sql .= ' f.multicurrency_total_tva, f.multicurrency_total_ttc,';
 	$sql .= ' s.rowid, s.nom, s.email, s.town, s.zip, s.fk_pays, s.client, s.fournisseur, s.code_client, s.code_fournisseur, s.code_compta, s.code_compta_fournisseur,';
 	$sql .= ' typent.code,';
 	$sql .= ' state.code_departement, state.nom,';

+ 8 - 3
htdocs/compta/facture/prelevement.php

@@ -38,8 +38,6 @@ require_once DOL_DOCUMENT_ROOT.'/societe/class/companybankaccount.class.php';
 require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.class.php';
 require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php';
 
-if (!$user->rights->facture->lire) accessforbidden();
-
 // Load translation files required by the page
 $langs->loadLangs(array('bills', 'banks', 'withdrawals', 'companies'));
 
@@ -51,7 +49,6 @@ $type = GETPOST('type', 'aZ09');
 
 $fieldid = (!empty($ref) ? 'ref' : 'rowid');
 if ($user->socid) $socid = $user->socid;
-$result = restrictedArea($user, 'facture', $id, '', '', 'fk_soc', $fieldid);
 
 if ($type == 'bank-transfer') {
 	$object = new FactureFournisseur($db);
@@ -63,6 +60,7 @@ if ($type == 'bank-transfer') {
 if ($id > 0 || !empty($ref))
 {
 	$ret = $object->fetch($id, $ref);
+	$isdraft = (($object->statut == FactureFournisseur::STATUS_DRAFT) ? 1 : 0);
 	if ($ret > 0)
 	{
 		$object->fetch_thirdparty();
@@ -71,6 +69,13 @@ if ($id > 0 || !empty($ref))
 
 $hookmanager->initHooks(array('directdebitcard', 'globalcard'));
 
+if ($type == 'bank-transfer') {
+	$result = restrictedArea($user, 'fournisseur', $id, 'facture_fourn', 'facture', 'fk_soc', $fieldid, $isdraft);
+	if (!$user->rights->fournisseur->facture->lire) accessforbidden();
+} else {
+	$result = restrictedArea($user, 'facture', $id, '', '', 'fk_soc', $fieldid, $isdraft);
+	if (!$user->rights->facture->lire) accessforbidden();
+}
 
 
 /*

+ 10 - 2
htdocs/compta/index.php

@@ -2,7 +2,7 @@
 /* Copyright (C) 2001-2005 Rodolphe Quiedeville <rodolphe@quiedeville.org>
  * Copyright (C) 2004-2013 Laurent Destailleur  <eldy@users.sourceforge.net>
  * Copyright (C) 2005-2015 Regis Houssin        <regis.houssin@inodbox.com>
- * Copyright (C) 2015-2016 Juanjo Menent	<jmenent@2byte.es>
+ * Copyright (C) 2015-2020 Juanjo Menent	<jmenent@2byte.es>
  * Copyright (C) 2015      Jean-François Ferry	<jfefe@aternatik.fr>
  * Copyright (C) 2015      Raphaël Doursenaud   <rdoursenaud@gpcsolutions.fr>
  * Copyright (C) 2016      Marcos García        <marcosgdf@gmail.com>
@@ -161,6 +161,14 @@ if (!empty($conf->facture->enabled) && $user->rights->facture->lire)
 	$reshook = $hookmanager->executeHooks('printFieldListWhereCustomerDraft', $parameters);
 	$sql .= $hookmanager->resPrint;
 
+	$sql.= " GROUP BY f.rowid, f.ref, f.datef, f.total, f.tva, f.total_ttc, f.ref_client, f.type, ";
+	$sql.= "s.email, s.nom, s.rowid, s.code_client, s.code_compta, s.code_fournisseur, s.code_compta_fournisseur";
+
+	// Add Group from hooks
+	$parameters = array();
+	$reshook = $hookmanager->executeHooks('printFieldListGroupByCustomerDraft', $parameters);
+	$sql .= $hookmanager->resPrint;
+
 	$resql = $db->query($sql);
 
 	if ($resql)
@@ -458,7 +466,7 @@ if ((!empty($conf->fournisseur->enabled) && empty($conf->global->MAIN_USE_NEW_SU
 	$sql .= $hookmanager->resPrint;
 
 	$sql .= " GROUP BY ff.rowid, ff.ref, ff.fk_statut, ff.libelle, ff.total_ht, ff.tva, ff.total_tva, ff.total_ttc, ff.tms, ff.paye,";
-	$sql .= " s.nom, s.rowid, s.code_fournisseur, s.code_compta_fournisseur";
+	$sql .= " s.nom, s.rowid, s.code_fournisseur, s.code_compta_fournisseur, s.email";
 	$sql .= " ORDER BY ff.tms DESC ";
 	$sql .= $db->plimit($max, 0);
 

+ 7 - 1
htdocs/contact/list.php

@@ -206,7 +206,13 @@ if (is_array($extrafields->attributes[$object->table_element]['label']) && count
 	foreach ($extrafields->attributes[$object->table_element]['label'] as $key => $val)
 	{
 		if (!empty($extrafields->attributes[$object->table_element]['list'][$key]))
-			$arrayfields["ef.".$key] = array('label'=>$extrafields->attributes[$object->table_element]['label'][$key], 'checked'=>(($extrafields->attributes[$object->table_element]['list'][$key] < 0) ? 0 : 1), 'position'=>$extrafields->attributes[$object->table_element]['pos'][$key], 'enabled'=>(abs($extrafields->attributes[$object->table_element]['list'][$key]) != 3 && $extrafields->attributes[$object->table_element]['perms'][$key]));
+			$arrayfields["ef.".$key] = array(
+				'label'=>$extrafields->attributes[$object->table_element]['label'][$key],
+				'checked'=>(($extrafields->attributes[$object->table_element]['list'][$key] < 0) ? 0 : 1),
+				'position'=>$extrafields->attributes[$object->table_element]['pos'][$key],
+				'enabled'=>(abs($extrafields->attributes[$object->table_element]['list'][$key]) != 3 && $extrafields->attributes[$object->table_element]['perms'][$key]),
+				'langfile'=>$extrafields->attributes[$object->table_element]['langfile'][$key],
+			);
 	}
 }
 $object->fields = dol_sort_array($object->fields, 'position');

+ 6 - 6
htdocs/core/class/CMailFile.class.php

@@ -355,8 +355,8 @@ class CMailFile
 			}
 
 			$smtps->setSubject($subjecttouse);
-			$smtps->setTO($this->getValidAddress($this->to, 0, 1));
-			$smtps->setFrom($this->getValidAddress($this->from, 0, 1));
+			$smtps->setTO($this->getValidAddress($this->addr_to, 0, 1));
+			$smtps->setFrom($this->getValidAddress($this->addr_from, 0, 1));
 			$smtps->setTrackId($this->trackid);
 			$smtps->setReplyTo($this->getValidAddress($this->replyto, 0, 1));
 
@@ -402,7 +402,7 @@ class CMailFile
 			$smtps->setDeliveryReceipt($this->deliveryreceipt);
 
 			$host = dol_getprefix('email');
-			$this->msgid = time().'.SMTPs-dolibarr-'.$trackid.'@'.$host;
+			$this->msgid = time().'.SMTPs-dolibarr-'.$this->trackid.'@'.$host;
 
 			$this->smtps = $smtps;
 		} elseif ($this->sendmode == 'swiftmailer') {
@@ -786,14 +786,14 @@ class CMailFile
 				$from = $this->smtps->getFrom('org');
 				if ($res && !$from)
 				{
-					$this->error = "Failed to send mail with smtps lib to HOST=".$server.", PORT=".$conf->global->$keyforsmtpport."<br>Sender address '$from' invalid";
+					$this->error = "Failed to send mail with smtps lib to HOST=".$server.", PORT=".$conf->global->$keyforsmtpport." - Sender address '$from' invalid";
 					dol_syslog("CMailFile::sendfile: mail end error=".$this->error, LOG_ERR);
 					$res = false;
 				}
 				$dest = $this->smtps->getTo();
 				if ($res && !$dest)
 				{
-					$this->error = "Failed to send mail with smtps lib to HOST=".$server.", PORT=".$conf->global->$keyforsmtpport."<br>Recipient address '$dest' invalid";
+					$this->error = "Failed to send mail with smtps lib to HOST=".$server.", PORT=".$conf->global->$keyforsmtpport." - Recipient address '$dest' invalid";
 					dol_syslog("CMailFile::sendfile: mail end error=".$this->error, LOG_ERR);
 					$res = false;
 				}
@@ -814,7 +814,7 @@ class CMailFile
 						$res = true;
 					} else {
 						if (empty($this->error)) $this->error = $result;
-						dol_syslog("CMailFile::sendfile: mail end error with smtps lib to HOST=".$server.", PORT=".$conf->global->$keyforsmtpport."<br>".$this->error, LOG_ERR);
+						dol_syslog("CMailFile::sendfile: mail end error with smtps lib to HOST=".$server.", PORT=".$conf->global->$keyforsmtpport." - ".$this->error, LOG_ERR);
 						$res = false;
 					}
 				}

+ 75 - 0
htdocs/core/class/fiscalyear.class.php

@@ -35,6 +35,8 @@ class Fiscalyear extends CommonObject
 	 */
 	public $element = 'fiscalyear';
 
+	public $picto = 'technic';
+
 	/**
 	 * @var string Name of table without prefix where object is stored
 	 */
@@ -97,6 +99,7 @@ class Fiscalyear extends CommonObject
 	public $statuts = array();
 	public $statuts_short = array();
 
+
 	/**
 	 * Constructor
 	 *
@@ -267,6 +270,78 @@ class Fiscalyear extends CommonObject
 		}
 	}
 
+	/**
+	 *	Return clicable link of object (with eventually picto)
+	 *
+	 *	@param      int			$withpicto                Add picto into link
+	 *  @param	    int   	    $notooltip		          1=Disable tooltip
+	 *  @param      int         $save_lastsearch_value    -1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values whenclicking
+	 *	@return     string          			          String with URL
+	 */
+	public function getNomUrl($withpicto = 0, $notooltip = 0, $save_lastsearch_value = -1)
+	{
+		global $conf, $langs, $user;
+
+		if (empty($this->ref)) $this->ref = $this->id;
+
+		if (!empty($conf->dol_no_mouse_hover)) $notooltip = 1; // Force disable tooltips
+
+		$result = '';
+
+		$url = DOL_URL_ROOT.'/accountancy/admin/fiscalyear_card.php?id='.$this->id;
+
+		if (!$user->rights->accounting->fiscalyear->write)
+			$option = 'nolink';
+
+		if ($option !== 'nolink')
+		{
+			// Add param to save lastsearch_values or not
+			$add_save_lastsearch_values = ($save_lastsearch_value == 1 ? 1 : 0);
+			if ($save_lastsearch_value == -1 && preg_match('/list\.php/', $_SERVER["PHP_SELF"])) $add_save_lastsearch_values = 1;
+			if ($add_save_lastsearch_values) $url .= '&save_lastsearch_values=1';
+		}
+
+		if ($short) return $url;
+
+		$label = '';
+
+		if ($user->rights->accounting->fiscalyear->write) {
+			$label = '<u>'.$langs->trans("FiscalPeriod").'</u>';
+			$label .= '<br><b>'.$langs->trans('Ref').':</b> '.$this->id;
+			if (isset($this->statut)) {
+				$label .= '<br><b>'.$langs->trans("Status").":</b> ".$this->getLibStatut(5);
+			}
+		}
+
+		$linkclose = '';
+		if (empty($notooltip) && $user->rights->accounting->fiscalyear->write)
+		{
+			if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER))
+			{
+				$label = $langs->trans("FiscalYear");
+				$linkclose .= ' alt="'.dol_escape_htmltag($label, 1).'"';
+			}
+			$linkclose .= ' title="'.dol_escape_htmltag($label, 1).'"';
+			$linkclose .= ' class="classfortooltip"';
+		}
+
+		$linkstart = '<a href="'.$url.'"';
+		$linkstart .= $linkclose.'>';
+		$linkend = '</a>';
+
+		if ($option === 'nolink') {
+			$linkstart = '';
+			$linkend = '';
+		}
+
+		$result .= $linkstart;
+		if ($withpicto) $result .= img_object(($notooltip ? '' : $label), $this->picto, ($notooltip ? (($withpicto != 2) ? 'class="paddingright"' : '') : 'class="'.(($withpicto != 2) ? 'paddingright ' : '').'classfortooltip"'), 0, 0, $notooltip ? 0 : 1);
+		if ($withpicto != 2) $result .= $this->ref;
+		$result .= $linkend;
+
+		return $result;
+	}
+
 	/**
 	 * Give a label from a status
 	 *

+ 6 - 2
htdocs/core/class/html.form.class.php

@@ -5830,7 +5830,7 @@ class Form
 			$urlforajaxcall = DOL_URL_ROOT.'/core/ajax/selectobject.php';
 
 			// No immediate load of all database
-			$urloption = 'htmlname='.$htmlname.'&outjson=1&objectdesc='.$objectdesc.'&filter='.urlencode($objecttmp->filter).($moreparams ? $moreparams : '');
+			$urloption = 'htmlname='.$htmlname.'&outjson=1&objectdesc='.$objectdesc.'&filter='.urlencode($objecttmp->filter);
 			// Activate the auto complete using ajax call.
 			$out .= ajax_autocompleter($preselectedvalue, $htmlname, $urlforajaxcall, $urloption, $conf->global->$confkeyforautocompletemode, 0, array());
 			$out .= '<style type="text/css">.ui-autocomplete { z-index: 250; }</style>';
@@ -6496,7 +6496,7 @@ class Form
 	 */
 	public static function multiSelectArrayWithCheckbox($htmlname, &$array, $varpage)
 	{
-		global $conf, $langs, $user;
+		global $conf, $langs, $user, $extrafields;
 
 		if (!empty($conf->global->MAIN_OPTIMIZEFORTEXTBROWSER)) return '';
 
@@ -6528,6 +6528,10 @@ class Form
 		    }
 		    if ($val['label'])
 		    {
+		    	if (! empty($val['langfile']) && is_object($langs)) {
+		    		$langs->load($val['langfile']);
+		    	}
+
 		        $lis .= '<li><input type="checkbox" id="checkbox'.$key.'" value="'.$key.'"'.(empty($val['checked']) ? '' : ' checked="checked"').'/><label for="checkbox'.$key.'">'.dol_escape_htmltag($langs->trans($val['label'])).'</label></li>';
 			    $listcheckedstring .= (empty($val['checked']) ? '' : $key.',');
 		    }

+ 14 - 13
htdocs/core/lib/ajax.lib.php

@@ -25,25 +25,26 @@
 
 
 /**
- *	Generic function that return javascript to add to a page to transform a common input field into an autocomplete field by calling an Ajax page (ex: /societe/ajaxcompanies.php).
- *  The HTML field must be an input text with id=search_$htmlname.
- *  This use the jQuery "autocomplete" function. If we want to use the select2, we must also convert the input into select on funcntions that call this method.
+ * Generic function that return javascript to add to a page to transform a common input field into an autocomplete field by calling an Ajax page (ex: /societe/ajaxcompanies.php).
+ * The HTML field must be an input text with id=search_$htmlname.
+ * This use the jQuery "autocomplete" function. If we want to use the select2, we must also convert the input into select on funcntions that call this method.
  *
- *  @param	string	$selected           Preselected value
- *	@param	string	$htmlname           HTML name of input field
- *	@param	string	$url                Ajax Url to call for request: /path/page.php. Must return a json array ('key'=>id, 'value'=>String shown into input field once selected, 'label'=>String shown into combo list)
- *  @param	string	$urloption			More parameters on URL request
- *  @param	int		$minLength			Minimum number of chars to trigger that Ajax search
- *  @param	int		$autoselect			Automatic selection if just one value
- *  @param	array   $ajaxoptions		Multiple options array
+ * @param string	$selected 			Preselected value
+ * @param string	$htmlname 			HTML name of input field
+ * @param string	$url 				Ajax Url to call for request: /path/page.php. Must return a json array ('key'=>id, 'value'=>String shown into input field once selected, 'label'=>String shown into combo list)
+ * @param string	$urloption			More parameters on URL request
+ * @param int		$minLength			Minimum number of chars to trigger that Ajax search
+ * @param int		$autoselect			Automatic selection if just one value
+ * @param array		$ajaxoptions		Multiple options array
  *                                      - Ex: array('update'=>array('field1','field2'...)) will reset field1 and field2 once select done
  *                                      - Ex: array('disabled'=> )
  *                                      - Ex: array('show'=> )
  *                                      - Ex: array('update_textarea'=> )
  *                                      - Ex: array('option_disabled'=> id to disable and warning to show if we select a disabled value (this is possible when using autocomplete ajax)
- *	@return string              		Script
+ * @param string	$moreparams			More params provided to ajax call
+ * @return string   					Script
  */
-function ajax_autocompleter($selected, $htmlname, $url, $urloption = '', $minLength = 2, $autoselect = 0, $ajaxoptions = array())
+function ajax_autocompleter($selected, $htmlname, $url, $urloption = '', $minLength = 2, $autoselect = 0, $ajaxoptions = array(), $moreparams = '')
 {
     if (empty($minLength)) $minLength = 1;
 
@@ -55,7 +56,7 @@ function ajax_autocompleter($selected, $htmlname, $url, $urloption = '', $minLen
 
     // Input search_htmlname is original field
     // Input htmlname is a second input field used when using ajax autocomplete.
-	$script = '<input type="hidden" name="'.$htmlname.'" id="'.$htmlname.'" value="'.$selected.'" />';
+	$script = '<input type="hidden" name="'.$htmlname.'" id="'.$htmlname.'" value="'.$selected.'" '.($moreparams ? $moreparams : '').' />';
 
 	$script .= '<!-- Javascript code for autocomplete of field '.$htmlname.' -->'."\n";
 	$script .= '<script>'."\n";

+ 2 - 1
htdocs/core/lib/functions.lib.php

@@ -3850,7 +3850,7 @@ function dol_print_error($db = '', $error = '', $errors = null)
 		{
 			$out .= "<b>".$langs->trans("OS").":</b> ".php_uname()."<br>\n";
 		}
-		$out .= "<b>".$langs->trans("UserAgent").":</b> ".$_SERVER["HTTP_USER_AGENT"]."<br>\n";
+		$out .= "<b>".$langs->trans("UserAgent").":</b> ".dol_htmlentities($_SERVER["HTTP_USER_AGENT"], ENT_COMPAT, 'UTF-8')."<br>\n";
 		$out .= "<br>\n";
 		$out .= "<b>".$langs->trans("RequestedUrl").":</b> ".dol_htmlentities($_SERVER["REQUEST_URI"], ENT_COMPAT, 'UTF-8')."<br>\n";
 		$out .= "<b>".$langs->trans("Referer").":</b> ".(isset($_SERVER["HTTP_REFERER"]) ?dol_htmlentities($_SERVER["HTTP_REFERER"], ENT_COMPAT, 'UTF-8') : '')."<br>\n";
@@ -6071,6 +6071,7 @@ function getCommonSubstitutionArray($outputlangs, $onlykey = 0, $exclude = null,
 				$substitutionarray['__MEMBER_PHONE__'] = $object->phone;
 				$substitutionarray['__MEMBER_PHONEPRO__'] = $object->phone_perso;
 				$substitutionarray['__MEMBER_PHONEMOBILE__'] = $object->phone_mobile;
+				$substitutionarray['__MEMBER_TYPE__'] = $object->type;
 				$substitutionarray['__MEMBER_FIRST_SUBSCRIPTION_DATE__']       = dol_print_date($object->first_subscription_date, 'dayrfc');
 				$substitutionarray['__MEMBER_FIRST_SUBSCRIPTION_DATE_START__'] = dol_print_date($object->first_subscription_date_start, 'dayrfc');
 				$substitutionarray['__MEMBER_FIRST_SUBSCRIPTION_DATE_END__']   = dol_print_date($object->first_subscription_date_end, 'dayrfc');

+ 4 - 7
htdocs/core/lib/takepos.lib.php

@@ -48,13 +48,10 @@ function takepos_prepare_head()
 	$head[$h][2] = 'receipt';
 	$h++;
 
-	if ($conf->global->TAKEPOS_BAR_RESTAURANT)
-	{
-		$head[$h][0] = DOL_URL_ROOT.'/takepos/admin/bar.php';
-		$head[$h][1] = $langs->trans("BarRestaurant");
-		$head[$h][2] = 'bar';
-		$h++;
-	}
+	$head[$h][0] = DOL_URL_ROOT.'/takepos/admin/bar.php';
+	$head[$h][1] = $langs->trans("BarRestaurant");
+	$head[$h][2] = 'bar';
+	$h++;
 
 	$numterminals = max(1, $conf->global->TAKEPOS_NUM_TERMINALS);
 	for ($i = 1; $i <= $numterminals; $i++)

+ 1 - 1
htdocs/core/menus/standard/eldy.lib.php

@@ -1740,7 +1740,7 @@ function print_left_eldy_menu($db, $menu_array_before, $menu_array_after, &$tabM
 				if (empty($conf->global->PROJECT_HIDE_TASKS))
 				{
 					// Project affected to user
-					$newmenu->add("/projet/activity/index.php?leftmenu=tasks".($search_project_user ? '&search_project_user='.$search_project_user : ''), $langs->trans("Activities"), 0, $user->rights->projet->lire);
+					$newmenu->add("/projet/activity/index.php?leftmenu=tasks".($search_project_user ? '&search_project_user='.$search_project_user : ''), $langs->trans("Activities"), 0, $user->rights->projet->lire, '',  'project', 'tasks');
 					$newmenu->add("/projet/tasks.php?leftmenu=tasks&action=create", $langs->trans("NewTask"), 1, $user->rights->projet->creer);
 					$newmenu->add("/projet/tasks/list.php?leftmenu=tasks".($search_project_user ? '&search_project_user='.$search_project_user : ''), $langs->trans("List"), 1, $user->rights->projet->lire);
 				    $newmenu->add("/projet/tasks/stats/index.php?leftmenu=projects", $langs->trans("Statistics"), 1, $user->rights->projet->lire);

+ 1 - 1
htdocs/core/tpl/admin_extrafields_add.tpl.php

@@ -40,7 +40,7 @@ $langs->load("modulebuilder");
 ?>
 
 <!-- BEGIN PHP TEMPLATE admin_extrafields_add.tpl.php -->
-<script type="text/javascript">
+<script>
     jQuery(document).ready(function() {
     	function init_typeoffields(type)
     	{

+ 1 - 1
htdocs/core/tpl/admin_extrafields_edit.tpl.php

@@ -39,7 +39,7 @@ $langs->load("modulebuilder");
 ?>
 
 <!-- BEGIN PHP TEMPLATE admin_extrafields_edit.tpl.php -->
-<script type="text/javascript">
+<script>
     jQuery(document).ready(function() {
     	function init_typeoffields(type)
     	{

+ 1 - 1
htdocs/core/tpl/advtarget.tpl.php

@@ -19,7 +19,7 @@ if (!empty($conf->categorie->enabled) && $user->rights->categorie->lire) {
 	require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php';
 }
 
-print '<script type="text/javascript" language="javascript">
+print '<script>
 	$(document).ready(function() {
 
 		// Click Function

+ 1 - 1
htdocs/core/tpl/ajax/fileupload_main.tpl.php

@@ -25,7 +25,7 @@ if (empty($conf) || !is_object($conf))
 ?>
 
 <!-- START TEMPLATE FILE UPLOAD MAIN -->
-<script type="text/javascript">
+<script>
 window.locale = {
     "fileupload": {
         "errors": {

+ 1 - 1
htdocs/core/tpl/ajax/objectlinked_lineimport.tpl.php

@@ -31,7 +31,7 @@ if ($object->element == 'propal')
 ?>
 
 <!-- START TEMPLATE IMPORT OBJECT LINKED LINES -->
-<script type="text/javascript">
+<script>
 
 $(document).ready(function(){
 	$('.objectlinked_importbtn').click(function (e) {

+ 1 - 1
htdocs/core/tpl/bloc_showhide.tpl.php

@@ -32,7 +32,7 @@ if (isset($object->extraparams[$blocname]['showhide'])) $hide = (empty($object->
 <!-- BEGIN PHP TEMPLATE bloc_showhide.tpl.php -->
 
 <?php
-print '<script type="text/javascript">'."\n";
+print '<script>'."\n";
 print '$(document).ready(function() {'."\n";
 print '$("#hide-'.$blocname.'").click(function(){'."\n";
 print '		setShowHide(0);'."\n";

+ 1 - 1
htdocs/core/tpl/extrafields_view.tpl.php

@@ -207,7 +207,7 @@ if (empty($reshook) && is_array($extrafields->attributes[$object->table_element]
 	{
 		print "\n";
 		print '
-				<script type="text/javascript">
+				<script>
 				    jQuery(document).ready(function() {
 				    	function showOptions(child_list, parent_list)
 				    	{

+ 1 - 1
htdocs/core/tpl/filemanager.tpl.php

@@ -101,7 +101,7 @@ if ((!empty($conf->use_javascript_ajax) && empty($conf->global->MAIN_ECM_DISABLE
 	if ((empty($section) || $section == -1) && ($module != 'medias'))
 	{
 		?>
-		<script type="text/javascript">
+		<script>
     	jQuery(document).ready(function() {
 			jQuery('#<?php echo $nameforformuserfile ?>').hide();
     	});

+ 5 - 6
htdocs/core/tpl/login.tpl.php

@@ -79,7 +79,7 @@ if (!empty($conf->global->ADD_UNSPLASH_LOGIN_BACKGROUND)) {
 ?>
 
 <?php if (empty($conf->dol_use_jmobile)) { ?>
-<script type="text/javascript">
+<script>
 $(document).ready(function () {
 	/* Set focus on correct field */
 	<?php if ($focus_element) { ?>$('#<?php echo $focus_element; ?>').focus(); <?php } ?>		// Warning to use this only on visible element
@@ -356,23 +356,22 @@ if (!empty($conf->google->enabled) && !empty($conf->global->MAIN_GOOGLE_AN_ID))
 	}
 }
 
-// Google Adsense
+// TODO Replace this with a hook
+// Google Adsense (need Google module)
 if (!empty($conf->google->enabled) && !empty($conf->global->MAIN_GOOGLE_AD_CLIENT) && !empty($conf->global->MAIN_GOOGLE_AD_SLOT))
 {
 	if (empty($conf->dol_use_jmobile))
 	{
 		?>
 	<div class="center"><br>
-		<script type="text/javascript"><!--
+		<script><!--
 			google_ad_client = "<?php echo $conf->global->MAIN_GOOGLE_AD_CLIENT ?>";
 			google_ad_slot = "<?php echo $conf->global->MAIN_GOOGLE_AD_SLOT ?>";
 			google_ad_width = <?php echo $conf->global->MAIN_GOOGLE_AD_WIDTH ?>;
 			google_ad_height = <?php echo $conf->global->MAIN_GOOGLE_AD_HEIGHT ?>;
 			//-->
 		</script>
-		<script type="text/javascript"
-			src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
-		</script>
+		<script src="//pagead2.googlesyndication.com/pagead/show_ads.js"></script>
 	</div>
 		<?php
 	}

+ 50 - 3
htdocs/core/tpl/objectline_create.tpl.php

@@ -224,7 +224,7 @@ if ($nolinesbefore) {
 				if (!empty($conf->global->MAIN_AUTO_OPEN_SELECT2_ON_FOCUS_FOR_CUSTOMER_PRODUCTS))
 				{
 					?>
-				<script type="text/javascript">
+				<script>
 					$(document).ready(function(){
 						// On first focus on a select2 combo, auto open the menu (this allow to use the keyboard only)
 						$(document).on('focus', '.select2-selection.select2-selection--single', function (e) {
@@ -259,7 +259,7 @@ if ($nolinesbefore) {
 				if (!empty($conf->global->MAIN_AUTO_OPEN_SELECT2_ON_FOCUS_FOR_SUPPLIER_PRODUCTS))
 				{
 					?>
-				<script type="text/javascript">
+				<script>
 					$(document).ready(function(){
 						// On first focus on a select2 combo, auto open the menu (this allow to use the keyboard only)
 						$(document).on('focus', '.select2-selection.select2-selection--single', function (e) {
@@ -362,7 +362,7 @@ if ($nolinesbefore) {
 	if (!empty($conf->global->PRODUCT_USE_UNITS)) {
 		$coldisplay++;
 		print '<td class="nobottom linecoluseunit left">';
-		print $form->selectUnits($line->fk_unit, "units");
+		print $form->selectUnits(empty($line->fk_unit) ? $conf->global->PRODUCT_USE_UNITS : $line->fk_unit, "units");
 		print '</td>';
 	}
 	$remise_percent = $buyer->remise_percent;
@@ -423,6 +423,27 @@ if ((!empty($conf->service->enabled) || ($object->element == 'contrat')) && $dat
 	print '<td colspan="'.($coldisplay - (empty($conf->global->MAIN_VIEW_LINE_NUMBER) ? 0 : 1)).'">';
 	$date_start = dol_mktime(GETPOST('date_starthour'), GETPOST('date_startmin'), 0, GETPOST('date_startmonth'), GETPOST('date_startday'), GETPOST('date_startyear'));
 	$date_end = dol_mktime(GETPOST('date_starthour'), GETPOST('date_startmin'), 0, GETPOST('date_endmonth'), GETPOST('date_endday'), GETPOST('date_endyear'));
+
+	$prefillDates = false;
+
+	if (!empty($conf->global->MAIN_FILL_SERVICE_DATES_FROM_LAST_SERVICE_LINE) && ! empty($object->lines))
+    {
+	    for ($i = count($object->lines) - 1; $i >= 0; $i--)
+	    {
+		    $lastline = $object->lines[$i];
+
+		    if ($lastline->product_type == Product::TYPE_SERVICE && (! empty($lastline->date_start) || ! empty($lastline->date_end)))
+		    {
+			    $date_start_prefill = $lastline->date_start;
+			    $date_end_prefill = $lastline->date_end;
+
+			    $prefillDates = true;
+			    break;
+		    }
+	    }
+    }
+
+
 	if (!empty($object->element) && $object->element == 'contrat')
 	{
 		print $langs->trans("DateStartPlanned").' ';
@@ -435,7 +456,33 @@ if ((!empty($conf->service->enabled) || ($object->element == 'contrat')) && $dat
 		print ' '.$langs->trans('to').' ';
 		print $form->selectDate($date_end, 'date_end', empty($conf->global->MAIN_USE_HOURMIN_IN_DATE_RANGE) ? 0 : 1, empty($conf->global->MAIN_USE_HOURMIN_IN_DATE_RANGE) ? 0 : 1, 1, "addproduct", 1, 0);
 	};
+
+	if ($prefillDates)
+    {
+        echo ' <span class="small"><a href="#" id="prefill_service_dates">' . $langs->trans('FillWithLastServiceDates') .  '</a></span>';
+    }
+
 	print '<script>';
+
+	if ($prefillDates)
+	{
+		?>
+        function prefill_service_dates()
+        {
+            $('#date_start').val("<?php echo dol_escape_js(dol_print_date($date_start_prefill, '%d/%m/%Y')); ?>").trigger('change');
+            $('#date_end').val("<?php echo dol_escape_js(dol_print_date($date_end_prefill, '%d/%m/%Y')); ?>").trigger('change');
+
+            return false; // Prevent default link behaviour (which is go to href URL)
+        }
+
+        $(document).ready(function()
+        {
+            $('#prefill_service_dates').click(prefill_service_dates);
+        });
+
+		<?php
+	}
+
 	if (!$date_start) {
 		if (isset($conf->global->MAIN_DEFAULT_DATE_START_HOUR)) {
 			print 'jQuery("#date_starthour").val("'.$conf->global->MAIN_DEFAULT_DATE_START_HOUR.'");';

+ 2 - 2
htdocs/core/tpl/objectline_edit.tpl.php

@@ -272,7 +272,7 @@ if (!empty($extrafields))
 	print $form->selectDate($line->date_start, 'date_start', $hourmin, $hourmin, $line->date_start ? 0 : 1, "updateline", 1, 0);
 	print ' '.$langs->trans('to').' ';
 	print $form->selectDate($line->date_end, 'date_end', $hourmin, $hourmin, $line->date_end ? 0 : 1, "updateline", 1, 0);
-	print '<script type="text/javascript">';
+	print '<script>';
 	if (!$line->date_start) {
 		if (isset($conf->global->MAIN_DEFAULT_DATE_START_HOUR)) {
 			print 'jQuery("#date_starthour").val("'.$conf->global->MAIN_DEFAULT_DATE_START_HOUR.'");';
@@ -297,7 +297,7 @@ if (!empty($extrafields))
 ?>
 
 
-<script type="text/javascript">
+<script>
 
 jQuery(document).ready(function()
 {

+ 1 - 1
htdocs/core/tpl/onlinepaymentlinks.tpl.php

@@ -148,7 +148,7 @@ if (!empty($conf->don->enabled))
 
 if (!empty($conf->use_javascript_ajax))
 {
-	print "\n".'<script type="text/javascript">';
+	print "\n".'<script>';
 	print '$(document).ready(function () {
 		$("#generate_token").click(function() {
             	$.get( "'.DOL_URL_ROOT.'/core/ajax/security.php", {

+ 5 - 6
htdocs/core/tpl/passwordforgotten.tpl.php

@@ -58,7 +58,7 @@ $colorbackhmenu1 = join(',', colorStringToArray($colorbackhmenu1)); // Normalize
 <body class="body bodylogin"<?php print empty($conf->global->MAIN_LOGIN_BACKGROUND) ? '' : ' style="background-size: cover; background-position: center center; background-attachment: fixed; background-repeat: no-repeat; background-image: url(\''.DOL_URL_ROOT.'/viewimage.php?cache=1&noalt=1&modulepart=mycompany&file='.urlencode('logos/'.$conf->global->MAIN_LOGIN_BACKGROUND).'\')"'; ?>>
 
 <?php if (empty($conf->dol_use_jmobile)) { ?>
-<script type="text/javascript">
+<script>
 $(document).ready(function () {
 	// Set focus on correct field
 	<?php if ($focus_element) { ?>$('#<?php echo $focus_element; ?>').focus(); <?php } ?>		// Warning to use this only on visible element
@@ -246,23 +246,22 @@ if (!empty($conf->google->enabled) && !empty($conf->global->MAIN_GOOGLE_AN_ID))
 	}
 }
 
-// Google Adsense
+// TODO Replace this with a hook
+// Google Adsense (need Google module)
 if (!empty($conf->google->enabled) && !empty($conf->global->MAIN_GOOGLE_AD_CLIENT) && !empty($conf->global->MAIN_GOOGLE_AD_SLOT))
 {
 	if (empty($conf->dol_use_jmobile))
 	{
 		?>
 	<div class="center"><br>
-		<script type="text/javascript"><!--
+		<script><!--
 			google_ad_client = "<?php echo $conf->global->MAIN_GOOGLE_AD_CLIENT ?>";
 			google_ad_slot = "<?php echo $conf->global->MAIN_GOOGLE_AD_SLOT ?>";
 			google_ad_width = <?php echo $conf->global->MAIN_GOOGLE_AD_WIDTH ?>;
 			google_ad_height = <?php echo $conf->global->MAIN_GOOGLE_AD_HEIGHT ?>;
 			//-->
 		</script>
-		<script type="text/javascript"
-			src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
-		</script>
+		<script	src="//pagead2.googlesyndication.com/pagead/show_ads.js"></script>
 	</div>
 		<?php
 	}

+ 2 - 2
htdocs/holiday/card.php

@@ -1299,7 +1299,7 @@ if ((empty($id) && empty($ref)) || $action == 'add' || $action == 'request' || $
 	                print '</tr>';
                 }
 
-                // Validator
+                // Approver
                 if (!$edit && $action != 'editvalidator') {
                     print '<tr>';
                     print '<td class="titlefield">';
@@ -1310,7 +1310,7 @@ if ((empty($id) && empty($ref)) || $action == 'add' || $action == 'request' || $
                     $include_users = $object->fetch_users_approver_holiday();
                     if (is_array($include_users) && in_array($user->id, $include_users) && $object->statut == Holiday::STATUS_VALIDATED)
                     {
-                        print '<a class="editfielda" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&action=editvalidator">'.img_edit($langs->trans("Edit")).'</a>';
+                        print '<a class="editfielda paddingleft" href="'.$_SERVER["PHP_SELF"].'?id='.$object->id.'&action=editvalidator">'.img_edit($langs->trans("Edit")).'</a>';
                     }
                     print '</td>';
                     print '</tr>';

+ 17 - 0
htdocs/install/mysql/migration/12.0.0-13.0.0.sql

@@ -175,6 +175,16 @@ ALTER TABLE llx_recruitment_recruitmentcandidature_extrafields ADD INDEX idx_fk_
 
 
 
+CREATE TABLE llx_product_attribute_combination_price_level
+(
+  rowid INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT,
+  fk_product_attribute_combination INTEGER DEFAULT 1 NOT NULL,
+  fk_price_level INTEGER DEFAULT 1 NOT NULL,
+  variation_price DOUBLE(24,8) NOT NULL,
+  variation_price_percentage INTEGER NULL
+)ENGINE=innodb;
+
+ALTER TABLE llx_product_attribute_combination_price_level ADD UNIQUE( fk_product_attribute_combination, fk_price_level);
 
 
 
@@ -219,3 +229,10 @@ create table llx_c_recruitment_origin
   label 	    varchar(64)	NOT NULL,
   active  	    tinyint DEFAULT 1  NOT NULL
 )ENGINE=innodb;
+
+
+
+ALTER TABLE llx_product MODIFY COLUMN seuil_stock_alerte float;
+ALTER TABLE llx_product MODIFY COLUMN desiredstock float;
+ALTER TABLE llx_product_warehouse_properties MODIFY COLUMN seuil_stock_alerte float; 
+ALTER TABLE llx_product_warehouse_properties MODIFY COLUMN desiredstock float; 

+ 2 - 2
htdocs/install/mysql/tables/llx_product.sql

@@ -60,7 +60,7 @@ create table llx_product
   tobatch                       tinyint      DEFAULT 0 NOT NULL,    -- Is it a product that need a batch management (eat-by or lot management)
   fk_product_type               integer      DEFAULT 0,             -- Type of product: 0 for regular product, 1 for service, 9 for other (used by external module)
   duration                      varchar(6),
-  seuil_stock_alerte            integer      DEFAULT NULL,
+  seuil_stock_alerte            float      DEFAULT NULL,
   url                           varchar(255),
   barcode                       varchar(180) DEFAULT NULL,          -- barcode
   fk_barcode_type               integer      DEFAULT NULL,          -- barcode type
@@ -96,7 +96,7 @@ create table llx_product
   import_key                    varchar(14),                        -- Import key
   model_pdf                     varchar(255),                       -- model save dodument used
   fk_price_expression           integer,                            -- Link to the rule for dynamic price calculation
-  desiredstock                  integer      DEFAULT 0,
+  desiredstock                  float      DEFAULT 0,
   fk_unit                       integer      DEFAULT NULL,
   price_autogen                 tinyint      DEFAULT 0,
   fk_project                    integer      DEFAULT NULL           -- Used when product was generated by a project or is specifif to a project

+ 28 - 0
htdocs/install/mysql/tables/llx_product_attribute_combination_price_level.sql

@@ -0,0 +1,28 @@
+-- ============================================================================
+-- Copyright (C) 2020      John BOTELLA         <john.botella@atm-consulting.fr>
+--
+-- 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 <https://www.gnu.org/licenses/>.
+--
+-- ============================================================================
+
+CREATE TABLE llx_product_attribute_combination_price_level
+(
+  rowid INTEGER PRIMARY KEY NOT NULL AUTO_INCREMENT,
+  fk_product_attribute_combination INTEGER DEFAULT 1 NOT NULL,
+  fk_price_level INTEGER DEFAULT 1 NOT NULL,
+  variation_price DOUBLE(24,8) NOT NULL,
+  variation_price_percentage INTEGER NULL
+)ENGINE=innodb;
+
+ALTER TABLE llx_product_attribute_combination_price_level ADD UNIQUE( fk_product_attribute_combination, fk_price_level);

+ 2 - 2
htdocs/install/mysql/tables/llx_product_stock_entrepot.sql

@@ -24,8 +24,8 @@ create table llx_product_warehouse_properties
   tms             		timestamp,
   fk_product      		integer NOT NULL,
   fk_entrepot     		integer NOT NULL,
-  seuil_stock_alerte    integer DEFAULT '0',
-  desiredstock    		integer DEFAULT '0',
+  seuil_stock_alerte    float DEFAULT '0',
+  desiredstock    		float DEFAULT '0',
   import_key      		varchar(14)               -- Import key
 )ENGINE=innodb;
 

+ 4 - 4
htdocs/langs/en_US/cashdesk.lang

@@ -77,7 +77,7 @@ POSModule=POS Module
 BasicPhoneLayout=Use basic layout for phones
 SetupOfTerminalNotComplete=Setup of terminal %s is not complete
 DirectPayment=Direct payment
-DirectPaymentButton=Direct cash payment button
+DirectPaymentButton=Add a "Direct cash payment" button
 InvoiceIsAlreadyValidated=Invoice is already validated
 NoLinesToBill=No lines to bill
 CustomReceipt=Custom Receipt
@@ -94,12 +94,12 @@ TakeposConnectorMethodDescription=External module with extra features. Posibilit
 PrintMethod=Print method
 ReceiptPrinterMethodDescription=Powerful method with a lot of parameters. Full customizable with templates. Cannot print from the cloud.
 ByTerminal=By terminal
-TakeposNumpadUsePaymentIcon=Use payment icon on numpad
+TakeposNumpadUsePaymentIcon=Use icon instead of text on payment buttons of numpad
 CashDeskRefNumberingModules=Numbering module for POS sales
 CashDeskGenericMaskCodes6 = <br><b>{TN}</b> tag is used to add the terminal number
 TakeposGroupSameProduct=Group same products lines
 StartAParallelSale=Start a new parallel sale
-ControlCashOpening=Control cash box at opening pos
+ControlCashOpening=Control cash box at opening POS
 CloseCashFence=Close cash fence
 CashReport=Cash report
 MainPrinterToUse=Main printer to use
@@ -117,6 +117,6 @@ HideCategoryImages=Hide Category Images
 HideProductImages=Hide Product Images
 NumberOfLinesToShow=Number of lines of images to show 
 DefineTablePlan=Define tables plan
-GiftReceiptButton=Gift receipt button
+GiftReceiptButton=Add a "Gift receipt" button
 GiftReceipt=Gift receipt
 ModuleReceiptPrinterMustBeEnabled=Module Receipt printer must have been enabled first

+ 2 - 2
htdocs/langs/en_US/main.lang

@@ -355,8 +355,8 @@ PriceUTTC=U.P. (inc. tax)
 Amount=Amount
 AmountInvoice=Invoice amount
 AmountInvoiced=Amount invoiced
-AmountInvoicedHT=Amount invoiced (incl. tax)
-AmountInvoicedTTC=Amount invoiced (excl. tax)
+AmountInvoicedHT=Amount invoiced (excl. tax)
+AmountInvoicedTTC=Amount invoiced (inc. tax)
 AmountPayment=Payment amount
 AmountHTShort=Amount (excl.)
 AmountTTCShort=Amount (inc. tax)

+ 4 - 0
htdocs/langs/en_US/products.lang

@@ -104,6 +104,7 @@ SetDefaultBarcodeType=Set barcode type
 BarcodeValue=Barcode value
 NoteNotVisibleOnBill=Note (not visible on invoices, proposals...)
 ServiceLimitedDuration=If product is a service with limited duration:
+FillWithLastServiceDates=Fill with last service line dates
 MultiPricesAbility=Multiple price segments per product/service (each customer is in one price segment)
 MultiPricesNumPrices=Number of prices
 DefaultPriceType=Base of prices per default (with versus without tax) when adding new sale prices
@@ -361,6 +362,9 @@ SelectCombination=Select combination
 ProductCombinationGenerator=Variants generator
 Features=Features
 PriceImpact=Price impact
+ImpactOnPriceLevel=Impact on price level %s
+ApplyToAllPriceImpactLevel= Apply to all levels
+ApplyToAllPriceImpactLevelHelp=By clicking here you set the same price impact on all levels
 WeightImpact=Weight impact
 NewProductAttribute=New attribute
 NewProductAttributeValue=New attribute value

+ 3 - 3
htdocs/langs/en_US/projects.lang

@@ -211,9 +211,9 @@ ProjectNbProjectByMonth=No. of created projects by month
 ProjectNbTaskByMonth=No. of created tasks by month
 ProjectOppAmountOfProjectsByMonth=Amount of leads by month
 ProjectWeightedOppAmountOfProjectsByMonth=Weighted amount of leads by month
-ProjectOpenedProjectByOppStatus=Open project/lead by lead status
-ProjectsStatistics=Statistics on projects/leads
-TasksStatistics=Statistics on project/lead tasks
+ProjectOpenedProjectByOppStatus=Open project|lead by lead status
+ProjectsStatistics=Statistics on projects or leads
+TasksStatistics=Statistics on tasks of projects or leads
 TaskAssignedToEnterTime=Task assigned. Entering time on this task should be possible.
 IdTaskTime=Id task time
 YouCanCompleteRef=If you want to complete the ref with some suffix, it is recommanded to add a - character to separate it, so the automatic numbering will still work correctly for next projects. For example %s-MYSUFFIX

+ 2 - 2
htdocs/langs/fr_FR/main.lang

@@ -355,8 +355,8 @@ PriceUTTC=P.U TTC
 Amount=Montant
 AmountInvoice=Montant facture
 AmountInvoiced=Montant facturé
-AmountInvoicedHT=Montant facturé (TTC)
-AmountInvoicedTTC=Montant facturé (HT)
+AmountInvoicedHT=Montant facturé (HT)
+AmountInvoicedTTC=Montant facturé (TTC)
 AmountPayment=Montant paiement
 AmountHTShort=Montant HT
 AmountTTCShort=Montant TTC

+ 2 - 1
htdocs/modulebuilder/template/myobject_list.php

@@ -147,7 +147,8 @@ if (is_array($extrafields->attributes[$object->table_element]['label']) && count
 				'label'=>$extrafields->attributes[$object->table_element]['label'][$key],
 				'checked'=>(($extrafields->attributes[$object->table_element]['list'][$key] < 0) ? 0 : 1),
 				'position'=>$extrafields->attributes[$object->table_element]['pos'][$key],
-				'enabled'=>(abs($extrafields->attributes[$object->table_element]['list'][$key]) != 3 && $extrafields->attributes[$object->table_element]['perms'][$key])
+				'enabled'=>(abs($extrafields->attributes[$object->table_element]['list'][$key]) != 3 && $extrafields->attributes[$object->table_element]['perms'][$key]),
+				'langfile'=>$extrafields->attributes[$object->table_element]['langfile'][$key]
 			);
 		}
 	}

+ 1 - 1
htdocs/product/card.php

@@ -1107,7 +1107,7 @@ if (is_object($objcanvas) && $objcanvas->displayCanvasExists($action))
 	    {
 		    print '<tr><td>'.$langs->trans('DefaultUnitToShow').'</td>';
 		    print '<td colspan="3">';
-		    print $form->selectUnits('', 'units');
+		    print $form->selectUnits(empty($line->fk_unit) ? $conf->global->PRODUCT_USE_UNITS : $line->fk_unit, 'units');
 		    print '</td></tr>';
 	    }
 

+ 118 - 104
htdocs/product/class/api_products.class.php

@@ -287,8 +287,8 @@ class Products extends DolibarrApi
         foreach ($request_data as $field => $value) {
             if ($field == 'id') { continue;
             }
-			if ($field == 'stock_reel') {
-				throw new RestException(400, 'Stock reel cannot be updated here. Use the /stockmovements endpoint instead');
+            if ($field == 'stock_reel') {
+                throw new RestException(400, 'Stock reel cannot be updated here. Use the /stockmovements endpoint instead');
             }
             $this->product->$field = $value;
         }
@@ -578,12 +578,12 @@ class Products extends DolibarrApi
         }
 
         if ($result > 0) {
-			require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
-			$prodcustprice = new Productcustomerprice($this->db);
-			$filter = array();
-			$filter['t.fk_product'] .= $id;
-			if ($thirdparty_id) $filter['t.fk_soc'] .= $thirdparty_id;
-			$result = $prodcustprice->fetch_all('', '', 0, 0, $filter);
+            require_once DOL_DOCUMENT_ROOT.'/product/class/productcustomerprice.class.php';
+            $prodcustprice = new Productcustomerprice($this->db);
+            $filter = array();
+            $filter['t.fk_product'] .= $id;
+            if ($thirdparty_id) $filter['t.fk_soc'] .= $thirdparty_id;
+            $result = $prodcustprice->fetch_all('', '', 0, 0, $filter);
         }
 
         if (empty($prodcustprice->lines)) {
@@ -624,8 +624,8 @@ class Products extends DolibarrApi
         }
 
         return array(
-        'prices_by_qty'=>$this->product->prices_by_qty[0], // 1 if price by quantity was activated for the product
-        'prices_by_qty_list'=>$this->product->prices_by_qty_list[0]
+            'prices_by_qty'=>$this->product->prices_by_qty[0], // 1 if price by quantity was activated for the product
+            'prices_by_qty_list'=>$this->product->prices_by_qty_list[0]
         );
     }
 
@@ -641,11 +641,11 @@ class Products extends DolibarrApi
      * @param	string		$ref_fourn			            Supplier ref
      * @param	float		$tva_tx				            New VAT Rate (For example 8.5. Should not be a string)
      * @param  	string		$charges			            costs affering to product
-	 * @param  	float		$remise_percent		            Discount  regarding qty (percent)
-	 * @param  	float		$remise				            Discount  regarding qty (amount)
-	 * @param  	int			$newnpr				            Set NPR or not
-	 * @param	int			$delivery_time_days	            Delay in days for delivery (max). May be '' if not defined.
-	 * @param   string      $supplier_reputation            Reputation with this product to the defined supplier (empty, FAVORITE, DONOTORDER)
+     * @param  	float		$remise_percent		            Discount  regarding qty (percent)
+     * @param  	float		$remise				            Discount  regarding qty (amount)
+     * @param  	int			$newnpr				            Set NPR or not
+     * @param	int			$delivery_time_days	            Delay in days for delivery (max). May be '' if not defined.
+     * @param   string      $supplier_reputation            Reputation with this product to the defined supplier (empty, FAVORITE, DONOTORDER)
      * @param   array		$localtaxes_array	            Array with localtaxes info array('0'=>type1,'1'=>rate1,'2'=>type2,'3'=>rate2) (loaded by getLocalTaxesFromRate(vatrate, 0, ...) function).
      * @param   string  	$newdefaultvatcode              Default vat code
      * @param  	float		$multicurrency_buyprice 	    Purchase price for the quantity min in currency
@@ -749,77 +749,77 @@ class Products extends DolibarrApi
      */
     public function getSupplierProducts($sortfield = "t.ref", $sortorder = 'ASC', $limit = 100, $page = 0, $mode = 0, $category = 0, $supplier = 0, $sqlfilters = '')
     {
-    	global $db, $conf;
-    	$obj_ret = array();
-    	$socid = DolibarrApiAccess::$user->socid ? DolibarrApiAccess::$user->socid : '';
-    	$sql = "SELECT t.rowid, t.ref, t.ref_ext";
-    	$sql .= " FROM ".MAIN_DB_PREFIX."product as t";
-    	if ($category > 0) {
-    		$sql .= ", ".MAIN_DB_PREFIX."categorie_product as c";
-    	}
-    	$sql .= ", ".MAIN_DB_PREFIX."product_fournisseur_price as s";
-
-    	$sql .= ' WHERE t.entity IN ('.getEntity('product').')';
-
-    	if ($supplier > 0) {
-    		$sql .= " AND s.fk_soc = ".$db->escape($supplier);
-    	}
-    	$sql .= " AND s.fk_product = t.rowid";
-    	// Select products of given category
-    	if ($category > 0) {
-    		$sql .= " AND c.fk_categorie = ".$db->escape($category);
-    		$sql .= " AND c.fk_product = t.rowid";
-    	}
-    	if ($mode == 1) {
-    		// Show only products
-    		$sql .= " AND t.fk_product_type = 0";
-    	} elseif ($mode == 2) {
-    		// Show only services
-    		$sql .= " AND t.fk_product_type = 1";
-    	}
-    	// Add sql filters
-    	if ($sqlfilters) {
-    		if (!DolibarrApi::_checkFilters($sqlfilters)) {
-    			throw new RestException(503, 'Error when validating parameter sqlfilters '.$sqlfilters);
-    		}
-    		$regexstring = '\(([^:\'\(\)]+:[^:\'\(\)]+:[^:\(\)]+)\)';
-    		$sql .= " AND (".preg_replace_callback('/'.$regexstring.'/', 'DolibarrApi::_forge_criteria_callback', $sqlfilters).")";
-    	}
-    	$sql .= $db->order($sortfield, $sortorder);
-    	if ($limit) {
-    		if ($page < 0) {
-    			$page = 0;
-    		}
-    		$offset = $limit * $page;
-    		$sql .= $db->plimit($limit + 1, $offset);
-    	}
-    	$result = $db->query($sql);
-    	if ($result) {
-    		$num = $db->num_rows($result);
-    		$min = min($num, ($limit <= 0 ? $num : $limit));
-    		$i = 0;
-    		while ($i < $min)
-    		{
-    			$obj = $db->fetch_object($result);
-
-    			$product_fourn = new ProductFournisseur($this->db);
-    			$product_fourn_list = $product_fourn->list_product_fournisseur_price($obj->rowid, '', '', 0, 0);
-    			foreach ($product_fourn_list as $tmpobj) {
-    				$this->_cleanObjectDatas($tmpobj);
-    			}
-
-   				//var_dump($product_fourn_list->db);exit;
-    			$obj_ret[$obj->rowid] = $product_fourn_list;
-
-    			$i++;
-    		}
-    	} else {
-    		throw new RestException(503, 'Error when retrieve product list : '.$db->lasterror());
-    	}
-    	if (!count($obj_ret)) {
-    		throw new RestException(404, 'No product found');
-    	}
-    	return $obj_ret;
+        global $db, $conf;
+        $obj_ret = array();
+        $socid = DolibarrApiAccess::$user->socid ? DolibarrApiAccess::$user->socid : '';
+        $sql = "SELECT t.rowid, t.ref, t.ref_ext";
+        $sql .= " FROM ".MAIN_DB_PREFIX."product as t";
+        if ($category > 0) {
+            $sql .= ", ".MAIN_DB_PREFIX."categorie_product as c";
+        }
+        $sql .= ", ".MAIN_DB_PREFIX."product_fournisseur_price as s";
+
+        $sql .= ' WHERE t.entity IN ('.getEntity('product').')';
+
+        if ($supplier > 0) {
+            $sql .= " AND s.fk_soc = ".$db->escape($supplier);
+        }
+        $sql .= " AND s.fk_product = t.rowid";
+        // Select products of given category
+        if ($category > 0) {
+            $sql .= " AND c.fk_categorie = ".$db->escape($category);
+            $sql .= " AND c.fk_product = t.rowid";
+        }
+        if ($mode == 1) {
+            // Show only products
+            $sql .= " AND t.fk_product_type = 0";
+        } elseif ($mode == 2) {
+            // Show only services
+            $sql .= " AND t.fk_product_type = 1";
+        }
+        // Add sql filters
+        if ($sqlfilters) {
+            if (!DolibarrApi::_checkFilters($sqlfilters)) {
+                throw new RestException(503, 'Error when validating parameter sqlfilters '.$sqlfilters);
+            }
+            $regexstring = '\(([^:\'\(\)]+:[^:\'\(\)]+:[^:\(\)]+)\)';
+            $sql .= " AND (".preg_replace_callback('/'.$regexstring.'/', 'DolibarrApi::_forge_criteria_callback', $sqlfilters).")";
+        }
+        $sql .= $db->order($sortfield, $sortorder);
+        if ($limit) {
+            if ($page < 0) {
+                $page = 0;
+            }
+            $offset = $limit * $page;
+            $sql .= $db->plimit($limit + 1, $offset);
+        }
+        $result = $db->query($sql);
+        if ($result) {
+            $num = $db->num_rows($result);
+            $min = min($num, ($limit <= 0 ? $num : $limit));
+            $i = 0;
+            while ($i < $min)
+            {
+                $obj = $db->fetch_object($result);
+
+                $product_fourn = new ProductFournisseur($this->db);
+                $product_fourn_list = $product_fourn->list_product_fournisseur_price($obj->rowid, '', '', 0, 0);
+                foreach ($product_fourn_list as $tmpobj) {
+                    $this->_cleanObjectDatas($tmpobj);
+                }
+
+                //var_dump($product_fourn_list->db);exit;
+                $obj_ret[$obj->rowid] = $product_fourn_list;
+
+                $i++;
+            }
+        } else {
+            throw new RestException(503, 'Error when retrieve product list : '.$db->lasterror());
+        }
+        if (!count($obj_ret)) {
+            throw new RestException(404, 'No product found');
+        }
+        return $obj_ret;
     }
 
     /**
@@ -1057,7 +1057,7 @@ class Products extends DolibarrApi
         $result = $prodattr->delete(DolibarrApiAccess::$user);
 
         if ($result <= 0) {
-        	throw new RestException(500, "Error deleting attribute");
+            throw new RestException(500, "Error deleting attribute");
         }
 
         return $result;
@@ -1161,9 +1161,23 @@ class Products extends DolibarrApi
             throw new RestException(401);
         }
 
-        $sql = "DELETE FROM ".MAIN_DB_PREFIX."product_attribute_value WHERE ref LIKE '".trim($ref)."' AND fk_product_attribute = ".(int) $id;
+        $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_attribute_value WHERE ref LIKE '".trim($ref)."' AND fk_product_attribute = ".(int) $id." AND entity IN (".getEntity('product').")";
+        $query = $this->db->query($sql);
+
+        if (!$query) {
+            throw new RestException(401);
+        }
+
+        if (!$this->db->num_rows($query)) {
+            throw new RestException(404, 'Attribute value not found');
+        }
+
+        $result = $this->db->fetch_object($query);
 
-        if ($this->db->query($sql)) {
+        $attrval = new ProductAttributeValue($this->db);
+        $attrval->id = $result->rowid;
+        $result = $attrval->delete(DolibarrApiAccess::$user);
+        if ($result > 0) {
             return 1;
         }
 
@@ -1328,7 +1342,7 @@ class Products extends DolibarrApi
         $objectval = new ProductAttributeValue($this->db);
         $objectval->id = (int) $id;
 
-        if ($objectval->delete() > 0) {
+        if ($objectval->delete(DolibarrApiAccess::$user) > 0) {
             return 1;
         }
         throw new RestException(500, "Error deleting attribute value");
@@ -1448,9 +1462,9 @@ class Products extends DolibarrApi
         $result = $prodcomb->createProductCombination(DolibarrApiAccess::$user, $this->product, $features, array(), $price_impact_is_percent, $price_impact, $weight_impact, $reference);
         if ($result > 0)
         {
-			return $result;
+            return $result;
         } else {
-			throw new RestException(500, "Error creating new product variant");
+            throw new RestException(500, "Error creating new product variant");
         }
     }
 
@@ -1573,7 +1587,7 @@ class Products extends DolibarrApi
         $result = $prodcomb->delete(DolibarrApiAccess::$user);
         if ($result <= 0)
         {
-        	throw new RestException(500, "Error deleting variant");
+            throw new RestException(500, "Error deleting variant");
         }
         return $result;
     }
@@ -1665,17 +1679,17 @@ class Products extends DolibarrApi
         }
 
         if ($includestockdata) {
-        	$this->product->load_stock();
-
-        	if (is_array($this->product->stock_warehouse)) {
-        		foreach ($this->product->stock_warehouse as $keytmp => $valtmp) {
-        			if (is_array($this->product->stock_warehouse[$keytmp]->detail_batch)) {
-        				foreach ($this->product->stock_warehouse[$keytmp]->detail_batch as $keytmp2 => $valtmp2) {
-        					unset($this->product->stock_warehouse[$keytmp]->detail_batch[$keytmp2]->db);
-        				}
-        			}
-        		}
-        	}
+            $this->product->load_stock();
+
+            if (is_array($this->product->stock_warehouse)) {
+                foreach ($this->product->stock_warehouse as $keytmp => $valtmp) {
+                    if (is_array($this->product->stock_warehouse[$keytmp]->detail_batch)) {
+                        foreach ($this->product->stock_warehouse[$keytmp]->detail_batch as $keytmp2 => $valtmp2) {
+                            unset($this->product->stock_warehouse[$keytmp]->detail_batch[$keytmp2]->db);
+                        }
+                    }
+                }
+            }
         }
 
         if ($includesubproducts) {

+ 4 - 4
htdocs/product/class/product.class.php

@@ -205,7 +205,7 @@ class Product extends CommonObject
     /**
      * Stock alert
      *
-     * @var int
+     * @var float
      */
     public $seuil_stock_alerte = 0;
 
@@ -995,7 +995,7 @@ class Product extends CommonObject
             $sql .= ", volume = ".($this->volume != '' ? "'".$this->db->escape($this->volume)."'" : 'null');
             $sql .= ", volume_units = ".($this->volume_units != '' ? "'".$this->db->escape($this->volume_units)."'" : 'null');
             $sql .= ", fk_default_warehouse = ".($this->fk_default_warehouse > 0 ? $this->db->escape($this->fk_default_warehouse) : 'null');
-            $sql .= ", seuil_stock_alerte = ".((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (int) $this->seuil_stock_alerte : 'null');
+            $sql .= ", seuil_stock_alerte = ".((isset($this->seuil_stock_alerte) && is_numeric($this->seuil_stock_alerte)) ? (float) $this->seuil_stock_alerte : 'null');
             $sql .= ", description = '".$this->db->escape($this->description)."'";
             $sql .= ", url = ".($this->url ? "'".$this->db->escape($this->url)."'" : 'null');
             $sql .= ", customcode = '".$this->db->escape($this->customcode)."'";
@@ -1008,7 +1008,7 @@ class Product extends CommonObject
             $sql .= ", accountancy_code_sell= '".$this->db->escape($this->accountancy_code_sell)."'";
             $sql .= ", accountancy_code_sell_intra= '".$this->db->escape($this->accountancy_code_sell_intra)."'";
             $sql .= ", accountancy_code_sell_export= '".$this->db->escape($this->accountancy_code_sell_export)."'";
-            $sql .= ", desiredstock = ".((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (int) $this->desiredstock : "null");
+            $sql .= ", desiredstock = ".((isset($this->desiredstock) && is_numeric($this->desiredstock)) ? (float) $this->desiredstock : "null");
             $sql .= ", cost_price = ".($this->cost_price != '' ? $this->db->escape($this->cost_price) : 'null');
             $sql .= ", fk_unit= ".(!$this->fk_unit ? 'NULL' : (int) $this->fk_unit);
             $sql .= ", price_autogen = ".(!$this->price_autogen ? 0 : 1);
@@ -1077,7 +1077,7 @@ class Product extends CommonObject
                         $comb = new ProductCombination($this->db);
 
                         foreach ($comb->fetchAllByFkProductParent($this->id) as $currcomb) {
-                                 $currcomb->updateProperties($this, $user);
+                        	$currcomb->updateProperties($this, $user);
                         }
                     }
 

+ 1 - 1
htdocs/product/composition/card.php

@@ -66,7 +66,7 @@ if ($id > 0 || !empty($ref))
 
 if ($cancel) $action = '';
 
-// Action association d'un sousproduit
+// Add subproduct to product
 if ($action == 'add_prod' && ($user->rights->produit->creer || $user->rights->service->creer))
 {
 	$error = 0;

+ 6 - 2
htdocs/product/stock/list.php

@@ -101,7 +101,9 @@ $search_all = trim(GETPOST("search_all", 'alpha'));
 $search = array();
 foreach ($object->fields as $key => $val)
 {
-	if (GETPOST('search_'.$key, 'alpha') !== '') $search[$key] = GETPOST('search_'.$key, 'alpha');
+	$search_key = $key;
+	if ($search_key == 'statut') $search_key = 'status'; // remove this after refactor entrepot.class property statut to status
+	if (GETPOST('search_'.$search_key, 'alpha') !== '') $search[$search_key] = GETPOST('search_'.$search_key, 'alpha');
 }
 
 // Definition of fields for list
@@ -225,13 +227,15 @@ if (!empty($conf->categorie->enabled))
 }
 foreach ($search as $key => $val)
 {
+	$class_key = $key;
+	if ($class_key == 'status') $class_key = 'statut'; // remove this after refactor entrepot.class property statut to status
 	if (($key == 'status' && $search[$key] == -1) || $key=='entity') continue;
 	$mode_search = (($object->isInt($object->fields[$key]) || $object->isFloat($object->fields[$key])) ? 1 : 0);
 	if (strpos($object->fields[$key]['type'], 'integer:') === 0) {
 		if ($search[$key] == '-1') $search[$key] = '';
 		$mode_search = 2;
 	}
-	if ($search[$key] != '') $sql .= natural_search((($key == 'ref') ? 't.ref' : 't.' . $key), $search[$key], (($key == 'status') ? 2 : $mode_search));
+	if ($search[$key] != '') $sql .= natural_search((($key == 'ref') ? 't.ref' : 't.' . $class_key), $search[$key], (($key == 'status') ? 2 : $mode_search));
 }
 if ($search_all) $sql .= natural_search(array_keys($fieldstosearchall), $search_all);
 // Add where from extra fields

+ 1 - 1
htdocs/projet/info.php

@@ -61,7 +61,7 @@ $search_agenda_label = GETPOST('search_agenda_label');
 $id = GETPOST("id", 'int');
 $socid = 0;
 //if ($user->socid > 0) $socid = $user->socid;    // For external user, no check is done on company because readability is managed by public status of project and assignement.
-$result = restrictedArea($user, 'projet', $id, '');
+$result = restrictedArea($user, 'projet', $id, 'projet&project');
 
 if (!$user->rights->projet->lire)	accessforbidden();
 

+ 2 - 4
htdocs/public/demo/index.php

@@ -417,16 +417,14 @@ if (!empty($conf->google->enabled) && !empty($conf->global->MAIN_GOOGLE_AD_CLIEN
 	if (empty($conf->dol_use_jmobile))
 	{
 		print '<div align="center">'."\n";
-		print '<script type="text/javascript"><!--'."\n";
+		print '<script><!--'."\n";
 		print 'google_ad_client = "'.$conf->global->MAIN_GOOGLE_AD_CLIENT.'";'."\n";
 		print 'google_ad_slot = "'.$conf->global->MAIN_GOOGLE_AD_SLOT.'";'."\n";
 		print 'google_ad_width = '.$conf->global->MAIN_GOOGLE_AD_WIDTH.';'."\n";
 		print 'google_ad_height = '.$conf->global->MAIN_GOOGLE_AD_HEIGHT.';'."\n";
 		print '//-->'."\n";
 		print '</script>'."\n";
-		print '<script type="text/javascript"'."\n";
-		print 'src="http://pagead2.googlesyndication.com/pagead/show_ads.js">'."\n";
-		print '</script>'."\n";
+		print '<script src="//pagead2.googlesyndication.com/pagead/show_ads.js"></script>'."\n";
 		print '</div>'."\n";
 	} else {
 		print '<!-- google js advert tag disabled with jmobile -->'."\n";

+ 128 - 113
htdocs/takepos/admin/bar.php

@@ -34,7 +34,8 @@ if (!$user->admin) accessforbidden();
 
 $langs->loadLangs(array("admin", "cashdesk", "printing"));
 
-global $db;
+$res = 0;
+
 
 /*
  * Actions
@@ -44,9 +45,15 @@ if (GETPOST('action', 'alpha') == 'set')
 {
 	$db->begin();
 
-	dol_syslog("admin/cashdesk: level ".GETPOST('level', 'alpha'));
+	dol_syslog("admin/bar");
+
+	$suplement_category = GETPOST('TAKEPOS_SUPPLEMENTS_CATEGORY', 'alpha');
+	if ($suplement_category < 0) $suplement_category= 0;
 
-	if (!$res > 0) $error++;
+	$res = dolibarr_set_const($db, "TAKEPOS_SUPPLEMENTS_CATEGORY", $suplement_category, 'chaine', 0, '', $conf->entity);
+	if ($res <= 0) {
+	    $error++;
+	}
 
  	if (!$error)
     {
@@ -74,7 +81,6 @@ $linkback = '<a href="'.DOL_URL_ROOT.'/admin/modules.php">'.$langs->trans("BackT
 print load_fiche_titre($langs->trans("CashDeskSetup").' (TakePOS)', $linkback, 'title_setup');
 $head = takepos_prepare_head();
 dol_fiche_head($head, 'bar', 'TakePOS', -1, 'cash-register');
-print '<br>';
 
 
 // Mode
@@ -93,129 +99,138 @@ function Floors() {
 
 <?php
 
-print '<a href="" onclick="Floors(); return false;"><span class="fa fa-glass-cheers"></span> '.$langs->trans("DefineTablePlan").'</a><br>';
-print '<br><br>';
+print $langs->trans("EnableBarOrRestaurantFeatures");
+print ajax_constantonoff("TAKEPOS_BAR_RESTAURANT", array(), $conf->entity, 0, 0, 1, 0);
 
-print '<div class="div-table-responsive">';
-print '<table class="noborder centpercent">';
-print '<tr class="liste_titre">';
-print '<td class="titlefieldcreate">'.$langs->trans("Parameters").'</td><td>'.$langs->trans("Value").'</td>';
-print "</tr>\n";
+print '<br>';
 
-if ($conf->global->TAKEPOS_BAR_RESTAURANT && $conf->global->TAKEPOS_PRINT_METHOD != "browser") {
-	print '<tr class="oddeven value"><td>';
-	print $langs->trans("OrderPrinters").' (<a href="'.DOL_URL_ROOT.'/takepos/admin/orderprinters.php?leftmenu=setup">'.$langs->trans("Setup").'</a>)';
-	print '</td>';
-	print '<td>';
-	print ajax_constantonoff("TAKEPOS_ORDER_PRINTERS", array(), $conf->entity, 0, 0, 1, 0);
-	//print $form->selectyesno("TAKEPOS_ORDER_PRINTERS", $conf->global->TAKEPOS_ORDER_PRINTERS, 1);
-	print '</td></tr>';
+if ($conf->global->TAKEPOS_BAR_RESTAURANT) {
+    print '<br>';
+    print '<a href="" onclick="Floors(); return false;"><span class="fa fa-glass-cheers"></span> '.$langs->trans("DefineTablePlan").'</a><br>';
+    print '<br><br>';
+
+    print '<div class="div-table-responsive-no-min">';
+    print '<table class="noborder centpercent">';
+    print '<tr class="liste_titre">';
+    print '<td class="titlefieldcreate">'.$langs->trans("Parameters").'</td><td>'.$langs->trans("Value").'</td>';
+    print "</tr>\n";
+
+    if ($conf->global->TAKEPOS_PRINT_METHOD != "browser") {
+    	print '<tr class="oddeven value"><td>';
+    	print $langs->trans("OrderPrinters").' (<a href="'.DOL_URL_ROOT.'/takepos/admin/orderprinters.php?leftmenu=setup">'.$langs->trans("Setup").'</a>)';
+    	print '</td>';
+    	print '<td>';
+    	print ajax_constantonoff("TAKEPOS_ORDER_PRINTERS", array(), $conf->entity, 0, 0, 1, 0);
+    	//print $form->selectyesno("TAKEPOS_ORDER_PRINTERS", $conf->global->TAKEPOS_ORDER_PRINTERS, 1);
+    	print '</td></tr>';
+
+    	print '<tr class="oddeven value"><td>';
+    	print $langs->trans("OrderNotes");
+    	print '</td>';
+    	print '<td>';
+    	print ajax_constantonoff("TAKEPOS_ORDER_NOTES", array(), $conf->entity, 0, 0, 1, 0);
+    	//print $form->selectyesno("TAKEPOS_ORDER_NOTES", $conf->global->TAKEPOS_ORDER_NOTES, 1);
+    	print '</td></tr>';
+    }
 
 	print '<tr class="oddeven value"><td>';
-	print $langs->trans("OrderNotes");
-	print '</td>';
-	print '<td>';
-	print ajax_constantonoff("TAKEPOS_ORDER_NOTES", array(), $conf->entity, 0, 0, 1, 0);
-	//print $form->selectyesno("TAKEPOS_ORDER_NOTES", $conf->global->TAKEPOS_ORDER_NOTES, 1);
-	print '</td></tr>';
-}
-
-print '<tr class="oddeven value"><td>';
-print $langs->trans("BasicPhoneLayout");
-print '</td>';
-print '<td>';
-//print $form->selectyesno("TAKEPOS_PHONE_BASIC_LAYOUT", $conf->global->TAKEPOS_PHONE_BASIC_LAYOUT, 1);
-print ajax_constantonoff("TAKEPOS_PHONE_BASIC_LAYOUT", array(), $conf->entity, 0, 0, 1, 0);
-print '</td></tr>';
-
-print '<tr class="oddeven value"><td>';
-print $langs->trans("ProductSupplements");
-print '</td>';
-print '<td>';
-//print $form->selectyesno("TAKEPOS_SUPPLEMENTS", $conf->global->TAKEPOS_SUPPLEMENTS, 1);
-print ajax_constantonoff("TAKEPOS_SUPPLEMENTS", array(), $conf->entity, 0, 0, 1, 0);
-print '</td></tr>';
-
-if ($conf->global->TAKEPOS_SUPPLEMENTS)
-{
-	print '<tr class="oddeven"><td>';
-	print $langs->trans("SupplementCategory");
-	print '</td>';
-	print '<td>';
-	print $form->select_all_categories(Categorie::TYPE_PRODUCT, $conf->global->TAKEPOS_SUPPLEMENTS_CATEGORY, 'TAKEPOS_SUPPLEMENTS_CATEGORY', 64, 0, 0);
-	print ajax_combobox('TAKEPOS_SUPPLEMENTS_CATEGORY');
-	print "</td></tr>\n";
-}
+    print $langs->trans("BasicPhoneLayout");
+    print '</td>';
+    print '<td>';
+    //print $form->selectyesno("TAKEPOS_PHONE_BASIC_LAYOUT", $conf->global->TAKEPOS_PHONE_BASIC_LAYOUT, 1);
+    print ajax_constantonoff("TAKEPOS_PHONE_BASIC_LAYOUT", array(), $conf->entity, 0, 0, 1, 0);
+    print '</td></tr>';
+
+    print '<tr class="oddeven value"><td>';
+    print $langs->trans("ProductSupplements");
+    print '</td>';
+    print '<td>';
+    //print $form->selectyesno("TAKEPOS_SUPPLEMENTS", $conf->global->TAKEPOS_SUPPLEMENTS, 1);
+    print ajax_constantonoff("TAKEPOS_SUPPLEMENTS", array(), $conf->entity, 0, 0, 1, 0);
+    print '</td></tr>';
+
+    if ($conf->global->TAKEPOS_SUPPLEMENTS)
+    {
+    	print '<tr class="oddeven"><td>';
+    	print $langs->trans("SupplementCategory");
+    	print '</td>';
+    	print '<td>';
+    	print $form->select_all_categories(Categorie::TYPE_PRODUCT, $conf->global->TAKEPOS_SUPPLEMENTS_CATEGORY, 'TAKEPOS_SUPPLEMENTS_CATEGORY', 64, 0, 0);
+    	print ajax_combobox('TAKEPOS_SUPPLEMENTS_CATEGORY');
+    	print "</td></tr>\n";
+    }
 
-print '<tr class="oddeven value"><td>';
-print 'QR - '.$langs->trans("CustomerMenu");
-print '</td>';
-print '<td>';
-print ajax_constantonoff("TAKEPOS_QR_MENU", array(), $conf->entity, 0, 0, 1, 0);
-print '</td></tr>';
+    print '<tr class="oddeven value"><td>';
+    print 'QR - '.$langs->trans("CustomerMenu");
+    print '</td>';
+    print '<td>';
+    print ajax_constantonoff("TAKEPOS_QR_MENU", array(), $conf->entity, 0, 0, 1, 0);
+    print '</td></tr>';
 
-print '<tr class="oddeven value"><td>';
-print 'QR - '.$langs->trans("AutoOrder");
-print '</td>';
-print '<td>';
-print ajax_constantonoff("TAKEPOS_AUTO_ORDER", array(), $conf->entity, 0, 0, 1, 0);
-print '</td></tr>';
+    print '<tr class="oddeven value"><td>';
+    print 'QR - '.$langs->trans("AutoOrder");
+    print '</td>';
+    print '<td>';
+    print ajax_constantonoff("TAKEPOS_AUTO_ORDER", array(), $conf->entity, 0, 0, 1, 0);
+    print '</td></tr>';
 
-print '</table>';
+    print '</table>';
+    print '</div>';
 
+    print '<br>';
 
-if ($conf->global->TAKEPOS_QR_MENU)
-{
-	$urlwithouturlroot = preg_replace('/'.preg_quote(DOL_URL_ROOT, '/').'$/i', '', trim($dolibarr_main_url_root));
-	$urlwithroot = $urlwithouturlroot.DOL_URL_ROOT; // This is to use external domain name found into config file
-	print '<br>';
-	print '<table class="noborder centpercent">';
-	print '<tr class="liste_titre">';
-	print '<td>'.$langs->trans("URL").'</td><td class="right">'.$langs->trans("QR").'</td>';
-	print "</tr>\n";
-	print '<tr class="oddeven value"><td>';
-	print "<a target='_blank' href='".$urlwithroot."/takepos/public/menu.php'>".$urlwithroot."/takepos/public/menu.php</a>";
-	print '</td>';
-	print '<td class="right">';
-	print "<a target='_blank' href='printqr.php'><img src='".DOL_URL_ROOT."/takepos/genimg/qr.php' height='42' width='42'></a>";
-	print '</td></tr>';
-	print '</table>';
+    print '<div class="center"><input type="submit" class="button" value="'.$langs->trans("Save").'"></div>';
 }
 
-if ($conf->global->TAKEPOS_AUTO_ORDER)
-{
-	print '<br>';
-	print '<table class="noborder centpercent">';
-	print '<tr class="liste_titre">';
-	print '<td>'.$langs->trans("Table").'</td><td>'.$langs->trans("URL").'</td><td class="right">'.$langs->trans("QR").'</td>';
-	print "</tr>\n";
-
-	//global $dolibarr_main_url_root;
-	$urlwithouturlroot = preg_replace('/'.preg_quote(DOL_URL_ROOT, '/').'$/i', '', trim($dolibarr_main_url_root));
-	$urlwithroot = $urlwithouturlroot.DOL_URL_ROOT; // This is to use external domain name found into config file
-	$sql = "SELECT rowid, entity, label, leftpos, toppos, floor FROM ".MAIN_DB_PREFIX."takepos_floor_tables";
-	$resql = $db->query($sql);
-	$rows = array();
-	while ($row = $db->fetch_array($resql)) {
-		print '<tr class="oddeven value"><td>';
-		print $langs->trans("Table")." ".$row['label'];
-		print '</td>';
-		print '<td>';
-		print "<a target='_blank' href='".$urlwithroot."/takepos/public/auto_order.php?key=".dol_encode($row['rowid'])."'>".$urlwithroot."/takepos/public/auto_order.php?key=".dol_encode($row['rowid'])."</a>";
-		print '</td>';
-		print '<td class="right">';
-		print "<a target='_blank' href='printqr.php?id=".$row['rowid']."'><img src='".DOL_URL_ROOT."/takepos/genimg/qr.php?key=".dol_encode($row['rowid'])."' height='42' width='42'></a>";
-		print '</td></tr>';
-	}
+if ($conf->global->TAKEPOS_BAR_RESTAURANT) {
+    if ($conf->global->TAKEPOS_QR_MENU)
+    {
+    	$urlwithouturlroot = preg_replace('/'.preg_quote(DOL_URL_ROOT, '/').'$/i', '', trim($dolibarr_main_url_root));
+    	$urlwithroot = $urlwithouturlroot.DOL_URL_ROOT; // This is to use external domain name found into config file
+    	print '<br>';
+    	print '<table class="noborder centpercent">';
+    	print '<tr class="liste_titre">';
+    	print '<td>'.$langs->trans("URL").'</td><td class="right">'.$langs->trans("QR").'</td>';
+    	print "</tr>\n";
+    	print '<tr class="oddeven value"><td>';
+    	print "<a target='_blank' href='".$urlwithroot."/takepos/public/menu.php'>".$urlwithroot."/takepos/public/menu.php</a>";
+    	print '</td>';
+    	print '<td class="right">';
+    	print "<a target='_blank' href='printqr.php'><img src='".DOL_URL_ROOT."/takepos/genimg/qr.php' height='42' width='42'></a>";
+    	print '</td></tr>';
+    	print '</table>';
+    }
 
-	print '</table>';
+    if ($conf->global->TAKEPOS_AUTO_ORDER)
+    {
+    	print '<br>';
+    	print '<table class="noborder centpercent">';
+    	print '<tr class="liste_titre">';
+    	print '<td>'.$langs->trans("Table").'</td><td>'.$langs->trans("URL").'</td><td class="right">'.$langs->trans("QR").'</td>';
+    	print "</tr>\n";
+
+    	//global $dolibarr_main_url_root;
+    	$urlwithouturlroot = preg_replace('/'.preg_quote(DOL_URL_ROOT, '/').'$/i', '', trim($dolibarr_main_url_root));
+    	$urlwithroot = $urlwithouturlroot.DOL_URL_ROOT; // This is to use external domain name found into config file
+    	$sql = "SELECT rowid, entity, label, leftpos, toppos, floor FROM ".MAIN_DB_PREFIX."takepos_floor_tables";
+    	$resql = $db->query($sql);
+    	$rows = array();
+    	while ($row = $db->fetch_array($resql)) {
+    		print '<tr class="oddeven value"><td>';
+    		print $langs->trans("Table")." ".$row['label'];
+    		print '</td>';
+    		print '<td>';
+    		print "<a target='_blank' href='".$urlwithroot."/takepos/public/auto_order.php?key=".dol_encode($row['rowid'])."'>".$urlwithroot."/takepos/public/auto_order.php?key=".dol_encode($row['rowid'])."</a>";
+    		print '</td>';
+    		print '<td class="right">';
+    		print "<a target='_blank' href='printqr.php?id=".$row['rowid']."'><img src='".DOL_URL_ROOT."/takepos/genimg/qr.php?key=".dol_encode($row['rowid'])."' height='42' width='42'></a>";
+    		print '</td></tr>';
+    	}
+
+    	print '</table>';
+    }
 }
 
-print '</div>';
-
-print '<br>';
-
-print '<div class="center"><input type="submit" class="button" value="'.$langs->trans("Save").'"></div>';
 
 print "</form>\n";
 

+ 13 - 0
htdocs/takepos/admin/receipt.php

@@ -87,6 +87,7 @@ print '<input type="hidden" name="action" value="set">';
 
 print load_fiche_titre($langs->trans("PrintMethod"), '', '');
 
+print '<div class="div-table-responsive-no-min">';
 print '<table class="noborder centpercent">';
 print '<tr class="liste_titre">';
 print '<td>'.$langs->trans("Name").'</td><td>'.$langs->trans("Description").'</td><td class="right">'.$langs->trans("Status").'</td>';
@@ -143,14 +144,25 @@ if ($conf->global->TAKEPOS_PRINT_METHOD == "takeposconnector")
 }
 print "</td></tr>\n";
 print '</table>';
+print '</div>';
+
 
 print load_fiche_titre($langs->trans("Setup"), '', '');
 
+print '<div class="div-table-responsive-no-min">';
 print '<table class="noborder centpercent">';
 print '<tr class="liste_titre">';
 print '<td>'.$langs->trans("Parameters").'</td><td>'.$langs->trans("Value").'</td>';
 print "</tr>\n";
 
+// VAT Grouped on ticket
+print '<tr class="oddeven"><td>';
+print $langs->trans('TicketVatGrouped');
+print '<td colspan="2">';
+print ajax_constantonoff("TAKEPOS_TICKET_VAT_GROUPPED", array(), $conf->entity, 0, 0, 1, 0);
+//print $form->selectyesno("TAKEPOS_TICKET_VAT_GROUPPED", $conf->global->TAKEPOS_TICKET_VAT_GROUPPED, 1);
+print "</td></tr>\n";
+
 if ($conf->global->TAKEPOS_PRINT_METHOD == "takeposconnector") {
 	print '<tr class="oddeven value"><td>';
 	print $langs->trans("URL")." / ".$langs->trans("IPAddress").' (<a href="http://en.takepos.com/connector" target="_blank">'.$langs->trans("TakeposConnectorNecesary").'</a>)';
@@ -214,6 +226,7 @@ print $form->selectyesno("TAKEPOS_AUTO_PRINT_TICKETS", $conf->global->TAKEPOS_AU
 print "</td></tr>\n";
 
 print '</table>';
+print '</div>';
 
 print '<br>';
 

+ 0 - 16
htdocs/takepos/admin/setup.php

@@ -274,14 +274,6 @@ print img_object('', 'category', 'class="paddingright"').$form->select_all_categ
 print ajax_combobox('TAKEPOS_ROOT_CATEGORY_ID');
 print "</td></tr>\n";
 
-// VAT Grouped on ticket
-print '<tr class="oddeven"><td>';
-print $langs->trans('TicketVatGrouped');
-print '<td colspan="2">';
-print ajax_constantonoff("TAKEPOS_TICKET_VAT_GROUPPED", array(), $conf->entity, 0, 0, 1, 0);
-//print $form->selectyesno("TAKEPOS_TICKET_VAT_GROUPPED", $conf->global->TAKEPOS_TICKET_VAT_GROUPPED, 1);
-print "</td></tr>\n";
-
 // Sort product
 print '<tr class="oddeven"><td>';
 print $langs->trans("SortProductField");
@@ -429,14 +421,6 @@ print "</td></tr>\n";
 //print $form->selectarray('TAKEPOS_ADDON', $array, (empty($conf->global->TAKEPOS_ADDON) ? '0' : $conf->global->TAKEPOS_ADDON), 0);
 //print "</td></tr>\n";
 
-print '<tr class="oddeven"><td>';
-print $langs->trans("EnableBarOrRestaurantFeatures");
-print '</td>';
-print '<td colspan="2">';
-print ajax_constantonoff("TAKEPOS_BAR_RESTAURANT", array(), $conf->entity, 0, 0, 1, 0);
-//print $form->selectyesno("TAKEPOS_BAR_RESTAURANT", $conf->global->TAKEPOS_BAR_RESTAURANT, 1);
-print "</td></tr>\n";
-
 print '</table>';
 print '</div>';
 

+ 1 - 0
htdocs/takepos/invoice.php

@@ -398,6 +398,7 @@ if ($action == "deleteline") {
         $invoice->deleteline($deletelineid);
         $invoice->fetch($placeid);
     }
+	if (count($invoice->lines)==0) $invoice->delete($user);
 }
 
 if ($action == "delete") {

+ 7 - 2
htdocs/theme/eldy/info-box.inc.php

@@ -77,7 +77,12 @@ if (!defined('ISLOADEDBYSTEELSHEET')) die('Must be call by steelsheet'); ?>
 	text-align: center;
 	font-size: 45px;
 	line-height: 90px;
-	background: rgba(0, 0, 0, 0.08) !important
+	background: rgba(0, 0, 0, 0.08) !important;
+}
+
+.info-box-module .info-box-icon {
+    padding-top: 5px;
+    padding-bottom: 5px;
 }
 .info-box-sm .info-box-icon {
     height: 80px;
@@ -436,7 +441,7 @@ if (GETPOSTISSET('THEME_SATURATE_RATIO')) $conf->global->THEME_SATURATE_RATIO =
 
 
 .info-box-module .info-box-content {
-	height: 6.4em;
+	height: 98px;
 }
 /* Disabled. This break the responsive on smartphone
 .box{

+ 1 - 1
htdocs/theme/eldy/theme_vars.inc.php

@@ -71,7 +71,7 @@ $colortexttitle = '0,0,0';
 $colortexttitlelink = '10, 20, 100';
 $colortext = '0,0,0';
 $colortextlink = '10, 20, 100';
-$fontsize = '0.86em';
+$fontsize = '0.95em';
 $fontsizesmaller = '0.75em';
 $topMenuFontSize = '1.1em';
 $toolTipBgColor = 'rgba(255, 255, 255, 0.96)';

+ 3 - 3
htdocs/variants/card.php

@@ -90,9 +90,9 @@ if ($confirm == 'yes') {
 	if ($action == 'confirm_delete') {
 		$db->begin();
 
-		$res = $objectval->deleteByFkAttribute($object->id);
+		$res = $objectval->deleteByFkAttribute($object->id, $user);
 
-		if ($res < 1 || ($object->delete() < 1)) {
+		if ($res < 1 || ($object->delete($user) < 1)) {
 			$db->rollback();
 			setEventMessages($langs->trans('CoreErrorMessage'), $object->errors, 'errors');
 			header('Location: '.dol_buildpath('/variants/card.php?id='.$object->id, 2));
@@ -105,7 +105,7 @@ if ($confirm == 'yes') {
 	} elseif ($action == 'confirm_deletevalue')
 	{
 		if ($objectval->fetch($valueid) > 0) {
-			if ($objectval->delete() < 1) {
+			if ($objectval->delete($user) < 1) {
 				setEventMessages($langs->trans('CoreErrorMessage'), $objectval->errors, 'errors');
 			} else {
 				setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');

+ 42 - 10
htdocs/variants/class/ProductAttribute.class.php

@@ -16,17 +16,18 @@
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
 
+require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
 /**
  * Class ProductAttribute
  * Used to represent a product attribute
  */
-class ProductAttribute
+class ProductAttribute extends CommonObject
 {
 	/**
 	 * Database handler
 	 * @var DoliDB
 	 */
-	private $db;
+	public $db;
 
 	/**
 	 * Id of the product attribute
@@ -119,7 +120,8 @@ class ProductAttribute
 
     			$return[] = $tmp;
     		}
-		} else dol_print_error($this->db);
+		}
+		else dol_print_error($this->db);
 
 		return $return;
 	}
@@ -127,11 +129,21 @@ class ProductAttribute
 	/**
 	 * Creates a product attribute
 	 *
-	 * @param	User	$user	Object user that create
+	 * @param   User    $user      Object user
+     * @param   int     $notrigger Do not execute trigger
 	 * @return 					int <0 KO, Id of new variant if OK
 	 */
-	public function create(User $user)
+	public function create(User $user, $notrigger = 0)
 	{
+	    if (empty($notrigger)) {
+	        // Call trigger
+	        $result = $this->call_trigger('PRODUCT_ATTRIBUTE_CREATE', $user);
+	        if ($result < 0) {
+	            return -1;
+	        }
+	        // End call triggers
+	    }
+
 		//Ref must be uppercase
 		$this->ref = strtoupper($this->ref);
 
@@ -152,11 +164,21 @@ class ProductAttribute
 	/**
 	 * Updates a product attribute
 	 *
-	 * @param	User	$user		Object user
+	 * @param   User    $user      Object user
+     * @param   int     $notrigger Do not execute trigger
 	 * @return 	int 				<0 KO, >0 OK
 	 */
-	public function update(User $user)
+	public function update(User $user, $notrigger = 0)
 	{
+	    if (empty($notrigger)) {
+	        // Call trigger
+	        $result = $this->call_trigger('PRODUCT_ATTRIBUTE_MODIFY', $user);
+	        if ($result < 0) {
+	            return -1;
+	        }
+	        // End call triggers
+	    }
+
 		//Ref must be uppercase
 		$this->ref = trim(strtoupper($this->ref));
 		$this->label = trim($this->label);
@@ -173,11 +195,21 @@ class ProductAttribute
 	/**
 	 * Deletes a product attribute
 	 *
-	 * @param	User	$user		Object user
-	 * @return 	int 				<0 KO, >0 OK
+	 * @param   User    $user      Object user
+     * @param   int     $notrigger Do not execute trigger
+	 * @return 	int <0 KO, >0 OK
 	 */
-	public function delete($user = null)
+	public function delete(User $user, $notrigger = 0)
 	{
+	    if (empty($notrigger)) {
+	        // Call trigger
+	        $result = $this->call_trigger('PRODUCT_ATTRIBUTE_DELETE', $user);
+	        if ($result < 0) {
+	            return -1;
+	        }
+	        // End call triggers
+	    }
+
 		$sql = "DELETE FROM ".MAIN_DB_PREFIX."product_attribute WHERE rowid = ".(int) $this->id;
 
 		if ($this->db->query($sql)) {

+ 76 - 25
htdocs/variants/class/ProductAttributeValue.class.php

@@ -16,17 +16,18 @@
  * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
 
+require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';
 /**
  * Class ProductAttributeValue
  * Used to represent a product attribute value
  */
-class ProductAttributeValue
+class ProductAttributeValue extends CommonObject
 {
 	/**
 	 * Database handler
 	 * @var DoliDB
 	 */
-	private $db;
+	public $db;
 
 	/**
 	 * Attribute value id
@@ -144,10 +145,11 @@ class ProductAttributeValue
 	/**
 	 * Creates a value for a product attribute
 	 *
-	 * @param	User	$user		Object user
-	 * @return 	int 				<0 KO >0 OK
+	 * @param  User $user      Object user
+     * @param  int  $notrigger Do not execute trigger
+	 * @return int <0 KO >0 OK
 	 */
-	public function create(User $user)
+	public function create(User $user, $notrigger = 0)
 	{
 		if (!$this->fk_product_attribute) {
 			return -1;
@@ -155,15 +157,25 @@ class ProductAttributeValue
 
 		// Ref must be uppercase
 		$this->ref = strtoupper($this->ref);
+	    $this->value = $this->db->escape($this->value);
 
 		$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.")";
+		'".$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');
+    		if (empty($notrigger)) {
+    		    // Call trigger
+    		    $result = $this->call_trigger('PRODUCT_ATTRIBUTE_VALUE_CREATE', $user);
+    		    if ($result < 0) {
+    		        return -1;
+    		    }
+    		    // End call triggers
+    		}
+
 			return 1;
 		}
 
@@ -173,11 +185,21 @@ class ProductAttributeValue
 	/**
 	 * Updates a product attribute value
 	 *
-	 * @param	User	$user	Object user
-	 * @return 	int				<0 if KO, >0 if OK
+	 * @param  User	$user	   Object user
+     * @param  int  $notrigger Do not execute trigger
+	 * @return int <0 if KO, >0 if OK
 	 */
-	public function update(User $user)
+	public function update(User $user, $notrigger = 0)
 	{
+	    if (empty($notrigger)) {
+	        // Call trigger
+	        $result = $this->call_trigger('PRODUCT_ATTRIBUTE_VALUE_MODIFY', $user);
+	        if ($result < 0) {
+	            return -1;
+	        }
+	        // End call triggers
+	    }
+
 		//Ref must be uppercase
 		$this->ref = trim(strtoupper($this->ref));
 		$this->value = trim($this->value);
@@ -196,33 +218,62 @@ class ProductAttributeValue
 	/**
 	 * Deletes a product attribute value
 	 *
+	 * @param  User $user      Object user
+     * @param  int  $notrigger Do not execute trigger
 	 * @return int <0 KO, >0 OK
 	 */
-	public function delete()
+	public function delete(User $user, $notrigger = 0)
 	{
-		$sql = "DELETE FROM ".MAIN_DB_PREFIX."product_attribute_value WHERE rowid = ".(int) $this->id;
 
-		if ($this->db->query($sql)) {
-			return 1;
-		}
-
-		return -1;
+	    if (empty($notrigger)) {
+	        // Call trigger
+	        $result = $this->call_trigger('PRODUCT_ATTRIBUTE_VALUE_DELETE', $user);
+	        if ($result < 0) {
+	            return -1;
+	        }
+	        // End call triggers
+	    }
+	    $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
+	 * @param int  $fk_attribute Product attribute id
+	 * @param User $user         Object user
 	 * @return int <0 KO, >0 OK
 	 */
-	public function deleteByFkAttribute($fk_attribute)
+	public function deleteByFkAttribute($fk_attribute, User $user)
 	{
-		$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;
+	    $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_attribute_value WHERE fk_product_attribute = ".(int) $fk_attribute;
+
+	    $query = $this->db->query($sql);
+
+	    if (!$query) {
+	        return -1;
+	    }
+
+	    if (!$this->db->num_rows($query)) {
+	        return 1;
+	    }
+
+	    while ($obj = $this->db->fetch_object($query)) {
+	        $tmp = new ProductAttributeValue($this->db);
+	        if ($tmp->fetch($obj->rowid) > 0) {
+    	        $result = $tmp->delete($user);
+    	        if ($result < 0) {
+    	            return -1;
+    	        }
+	        } else {
+	            return -1;
+	        }
+	    }
+
+	    return 1;
 	}
 }

+ 456 - 8
htdocs/variants/class/ProductCombination.class.php

@@ -71,6 +71,12 @@ class ProductCombination
 	 */
 	public $entity;
 
+	/**
+	 * Combination price level
+	 * @var ProductCombinationLevel[]
+	 */
+	public $combination_price_levels;
+
 	/**
 	 * Constructor
 	 *
@@ -92,6 +98,8 @@ class ProductCombination
 	 */
 	public function fetch($rowid)
 	{
+		global $conf;
+
 		$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 (".getEntity('product').")";
 
 		$query = $this->db->query($sql);
@@ -113,9 +121,118 @@ class ProductCombination
 		$this->variation_price_percentage = $obj->variation_price_percentage;
 		$this->variation_weight = $obj->variation_weight;
 
+		if (!empty($conf->global->PRODUIT_MULTIPRICES)) {
+			$this->fetchCombinationPriceLevels();
+		}
+
 		return 1;
 	}
 
+
+	/**
+	 * Retrieves combination price levels
+	 *
+	 * @param int $fk_price_level the price level to fetch, use 0 for all
+	 * @param bool $useCache to use cache or not
+	 * @return int <0 KO, >0 OK
+	 */
+	public function fetchCombinationPriceLevels($fk_price_level = 0, $useCache = true)
+	{
+		global $conf;
+
+		// Check cache
+		if (!empty($this->combination_price_levels) && $useCache){
+			if ((!empty($fk_price_level) && isset($this->combination_price_levels[$fk_price_level])) || empty($fk_price_level)){
+				return 1;
+			}
+		}
+
+		if (!is_array($this->combination_price_levels)
+			|| empty($fk_price_level) // if fetch an unique level dont erase all already fetched
+		){
+			$this->combination_price_levels = array();
+		}
+
+		$staticProductCombinationLevel = new ProductCombinationLevel($this->db);
+		$combination_price_levels = $staticProductCombinationLevel->fetchAll($this->id, $fk_price_level);
+
+		if (!is_array($combination_price_levels)) {
+			return -1;
+		}
+
+		if (empty($combination_price_levels)){
+
+			/**
+			 * for auto retrocompatibility with last behavior
+			 */
+			$productCombinationLevel = new ProductCombinationLevel($this->db);
+			$productCombinationLevel->fk_price_level = intval($fk_price_level);
+			$productCombinationLevel->fk_product_attribute_combination = $this->id;
+			$productCombinationLevel->variation_price = $this->variation_price;
+			$productCombinationLevel->variation_price_percentage = $this->variation_price_percentage;
+
+			if ($fk_price_level>0){
+				$combination_price_levels[$fk_price_level] = $productCombinationLevel;
+			}
+			else {
+				for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++){
+					$combination_price_levels[$i] = $productCombinationLevel;
+				}
+			}
+		}
+
+		$this->combination_price_levels = $combination_price_levels;
+
+		return 1;
+	}
+
+	/**
+	 * Retrieves combination price levels
+	 *
+	 * @param int $clean levels off PRODUIT_MULTIPRICES_LIMIT
+	 * @return int <0 KO, >0 OK
+	 */
+	public function saveCombinationPriceLevels($clean = 1)
+	{
+		global $conf;
+
+		$errors = 0;
+
+		$staticProductCombinationLevel = new ProductCombinationLevel($this->db);
+
+		// Delete all
+		if (empty($this->combination_price_levels)){
+			return $staticProductCombinationLevel->deleteAllForCombination($this->id);
+		}
+
+		// Clean not needed price levels
+		if ($clean){
+			$res = $staticProductCombinationLevel->clean($this->id);
+
+			if ($res<0){
+				$this->errors[] = 'Fail to clean not needed price levels';
+				return -1;
+			}
+		}
+
+		foreach ($this->combination_price_levels as $fk_price_level => $combination_price_level){
+			$res = $combination_price_level->save();
+			if ($res<1){
+				$this->error = 'save combination price level '.$fk_price_level . ' '.$combination_price_level->error;
+				$this->errors[] = $this->error;
+				$errors ++;
+			}
+		}
+
+		if ($errors > 0){
+			return $errors*-1;
+		}
+		else {
+			return 1;
+		}
+	}
+
+
 	/**
 	 * Retrieves a product combination by a child product row id
 	 *
@@ -124,6 +241,8 @@ class ProductCombination
 	 */
 	public function fetchByFkProductChild($fk_child)
 	{
+		global $conf;
+
 		$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 (".getEntity('product').")";
 
 		$query = $this->db->query($sql);
@@ -145,6 +264,10 @@ class ProductCombination
 		$this->variation_price_percentage = $result->variation_price_percentage;
 		$this->variation_weight = $result->variation_weight;
 
+		if (!empty($conf->global->PRODUIT_MULTIPRICES)) {
+			$this->fetchCombinationPriceLevels();
+		}
+
 		return 1;
 	}
 
@@ -156,6 +279,8 @@ class ProductCombination
 	 */
 	public function fetchAllByFkProductParent($fk_product_parent)
 	{
+		global $conf;
+
 		$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 (".getEntity('product').")";
 
 		$query = $this->db->query($sql);
@@ -175,6 +300,10 @@ class ProductCombination
 			$tmp->variation_price_percentage = $result->variation_price_percentage;
 			$tmp->variation_weight = $result->variation_weight;
 
+			if (!empty($conf->global->PRODUIT_MULTIPRICES)) {
+				$tmp->fetchCombinationPriceLevels();
+			}
+
 			$return[] = $tmp;
 		}
 
@@ -209,6 +338,8 @@ class ProductCombination
 	 */
 	public function create($user)
 	{
+		global $conf;
+
 		$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.",
@@ -221,6 +352,13 @@ class ProductCombination
 			return -1;
 		}
 
+		if (!empty($conf->global->PRODUIT_MULTIPRICES)) {
+			$res = $this->saveCombinationPriceLevels();
+			if ($res<0){
+				return -2;
+			}
+		}
+
 		$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX.'product_attribute_combination');
 
 		return 1;
@@ -234,6 +372,8 @@ class ProductCombination
 	 */
 	public function update(User $user)
 	{
+		global $conf;
+
 		$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.",
@@ -245,6 +385,14 @@ class ProductCombination
 			return -1;
 		}
 
+
+		if (!empty($conf->global->PRODUIT_MULTIPRICES)) {
+			$res = $this->saveCombinationPriceLevels();
+			if ($res<0){
+				return -2;
+			}
+		}
+
 		$parent = new Product($this->db);
 		$parent->fetch($this->fk_product_parent);
 
@@ -266,6 +414,12 @@ class ProductCombination
 		$comb2val = new ProductCombination2ValuePair($this->db);
 		$comb2val->deleteByFkCombination($this->id);
 
+		// remove combination price levels
+		if (!$this->db->query("DELETE FROM ".MAIN_DB_PREFIX."product_attribute_combination_price_level WHERE fk_product_attribute_combination = ".(int) $this->id)) {
+			$this->db->rollback();
+			return -1;
+		}
+
 		$sql = "DELETE FROM ".MAIN_DB_PREFIX."product_attribute_combination WHERE rowid = ".(int) $this->id;
 
 		if ($this->db->query($sql)) {
@@ -341,6 +495,7 @@ class ProductCombination
 			$child->label           = $parent->label.$varlabel;;
 		}
 
+
 		if ($child->update($child->id, $user) > 0) {
 			$new_vat = $parent->tva_tx;
 			$new_npr = $parent->tva_npr;
@@ -349,9 +504,12 @@ class ProductCombination
 			if (!empty($conf->global->PRODUIT_MULTIPRICES)) {
 				for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++)
 				{
-					if ($parent->multiprices[$i] != '') {
+					if ($parent->multiprices[$i] != '' || isset($this->combination_price_levels[$i]->variation_price)) {
 						$new_type = $parent->multiprices_base_type[$i];
 						$new_min_price = $parent->multiprices_min[$i];
+						$variation_price = doubleval(!isset($this->combination_price_levels[$i]->variation_price) ? $this->variation_price : $this->combination_price_levels[$i]->variation_price);
+						$variation_price_percentage = doubleval(!isset($this->combination_price_levels[$i]->variation_price_percentage) ? $this->variation_price_percentage : $this->combination_price_levels[$i]->variation_price_percentage);
+
 						if ($parent->prices_by_qty_list[$i]) {
 							$new_psq = 1;
 						} else {
@@ -364,12 +522,12 @@ class ProductCombination
 							$new_price = $parent->multiprices[$i];
 						}
 
-						if ($this->variation_price_percentage) {
+						if ($variation_price_percentage) {
 							if ($new_price != 0) {
-								$new_price *= 1 + ($this->variation_price / 100);
+								$new_price *= 1 + ($variation_price / 100);
 							}
 						} else {
-							$new_price += $this->variation_price;
+							$new_price += $variation_price;
 						}
 
 						$child->updatePrice($new_price, $new_type, $user, $new_vat, $new_min_price, $i, $new_npr, $new_psq);
@@ -508,7 +666,7 @@ WHERE c.fk_product_parent = ".(int) $productid." AND p.tosell = 1";
 	 * @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|array $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
 	 * @param bool|string $forced_refvar If the reference is forced
@@ -523,6 +681,8 @@ WHERE c.fk_product_parent = ".(int) $productid." AND p.tosell = 1";
 
 		$db->begin();
 
+		$price_impact = array(1=>0); // init level price impact
+
 		$forced_refvar = trim($forced_refvar);
 
 		if (!empty($forced_refvar) && $forced_refvar != $product->ref) {
@@ -545,7 +705,12 @@ WHERE c.fk_product_parent = ".(int) $productid." AND p.tosell = 1";
 		$weight_impact = (float) $forced_weightvar;	// If false, return 0
 
 		//Final price impact
-		$price_impact = (float) $forced_pricevar;	// If false, return 0
+		if (!is_array($forced_pricevar)){
+			$price_impact[1] = (float) $forced_pricevar;	// If false, return 0
+		}
+		else {
+			$price_impact = $forced_pricevar;
+		}
 
 		$newcomb = new ProductCombination($db);
 		$existingCombination = $newcomb->fetchByProductCombination2ValuePairs($product->id, $combinations);
@@ -587,7 +752,15 @@ WHERE c.fk_product_parent = ".(int) $productid." AND p.tosell = 1";
 				$weight_impact += (float) price2num($variations[$currcombattr][$currcombval]['weight']);
 			}
 			if ($forced_pricevar === false) {
-				$price_impact += (float) price2num($variations[$currcombattr][$currcombval]['price']);
+				$price_impact[1] += (float) price2num($variations[$currcombattr][$currcombval]['price']);
+
+				// Manage Price levels
+				if ($conf->global->PRODUIT_MULTIPRICES){
+					for ($i = 2; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++)
+					{
+						$price_impact[$i] += (float) price2num($variations[$currcombattr][$currcombval]['price']);
+					}
+				}
 			}
 
 			if ($forced_refvar === false) {
@@ -606,9 +779,27 @@ WHERE c.fk_product_parent = ".(int) $productid." AND p.tosell = 1";
 		}
 
 		$newcomb->variation_price_percentage = $price_var_percent;
-		$newcomb->variation_price = $price_impact;
+		$newcomb->variation_price = $price_impact[1];
 		$newcomb->variation_weight = $weight_impact;
 
+		// Init price level
+		if ($conf->global->PRODUIT_MULTIPRICES){
+			for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++){
+				$productCombinationLevel = new ProductCombinationLevel($this->db);
+				$productCombinationLevel->fk_product_attribute_combination = 0;
+				$productCombinationLevel->fk_price_level = $i;
+				$productCombinationLevel->variation_price = $price_impact[$i];
+
+				if (is_array($price_var_percent)){
+					$productCombinationLevel->variation_price_percentage = !empty($price_var_percent[$i]) ? $price_var_percent[$i] : 0;
+				}else {
+					$productCombinationLevel->variation_price_percentage = $price_var_percent;
+				}
+
+				$newcomb->combination_price_levels[$i] = $productCombinationLevel;
+			}
+		}
+
 		$newproduct->weight += $weight_impact;
 
 		// Now create the product
@@ -764,3 +955,260 @@ WHERE c.fk_product_parent = ".(int) $productid." AND p.tosell = 1";
 		return $label;
 	}
 }
+
+
+
+/**
+ * Class ProductCombinationLevel
+ * Used to represent a product combination Level
+ */
+class ProductCombinationLevel
+{
+	/**
+	 * Database handler
+	 * @var DoliDB
+	 */
+	private $db;
+
+	/**
+	 * @var string Name of table without prefix where object is stored
+	 */
+	public $table_element = 'product_attribute_combination_price_level';
+
+	/**
+	 * Rowid of combination
+	 * @var int
+	 */
+	public $id;
+
+	/**
+	 * Rowid of parent product combination
+	 * @var int
+	 */
+	public $fk_product_attribute_combination;
+
+	/**
+	 * Combination price level
+	 * @var int
+	 */
+	public $fk_price_level;
+
+	/**
+	 * Price variation
+	 * @var float
+	 */
+	public $variation_price;
+
+	/**
+	 * Is the price variation a relative variation?
+	 * @var bool
+	 */
+	public $variation_price_percentage = false;
+
+	/**
+	 * Constructor
+	 *
+	 * @param DoliDB $db Database handler
+	 */
+	public function __construct(DoliDB $db)
+	{
+		$this->db = $db;
+	}
+
+	/**
+	 * Retrieves a combination level by its rowid
+	 *
+	 * @param int $rowid Row id
+	 * @return int <0 KO, >0 OK
+	 */
+	public function fetch($rowid)
+	{
+		$sql = "SELECT rowid, fk_product_attribute_combination, fk_price_level, variation_price, variation_price_percentage FROM " . MAIN_DB_PREFIX . $this->table_element." WHERE rowid = " . (int) $rowid;
+
+		$obj = $this->db->getRow($sql);
+
+		if ($obj){
+			return $this->fetchFormObj($obj);
+		}
+
+		return -1;
+	}
+
+
+	/**
+	 * Retrieves combination price levels
+	 *
+	 * @param int $fk_product_attribute_combination
+	 * @param int $fk_price_level the price level to fetch, use 0 for all
+	 * @return self[] | -1 on KO
+	 */
+	public function fetchAll($fk_product_attribute_combination, $fk_price_level = 0)
+	{
+
+		$sql = "SELECT rowid, fk_product_attribute_combination, fk_price_level, variation_price, variation_price_percentage"
+			." FROM ".MAIN_DB_PREFIX.$this->table_element
+			." WHERE fk_product_attribute_combination = ".intval($fk_product_attribute_combination);
+
+		if (!empty($fk_price_level)){
+			$sql.= ' AND fk_price_level = '.intval($fk_price_level);
+		}
+
+		$combination_price_levels = $this->db->getRows($sql);
+
+		if (!is_array($combination_price_levels)) {
+			return -1;
+		}
+
+		$result = array();
+
+		if (!empty($combination_price_levels)) {
+			// For more simple usage set level as array key
+			foreach ($combination_price_levels as $k => $row){
+				$productCombinationLevel = new ProductCombinationLevel($this->db);
+				$productCombinationLevel->fetchFormObj($row);
+				$result[$row->fk_price_level] = $productCombinationLevel;
+			}
+		}
+
+		return $result;
+	}
+
+	/**
+	 * assign vars form an stdclass like sql obj
+	 *
+	 * @param int $rowid Row id
+	 * @return int <0 KO, >0 OK
+	 */
+	public function fetchFormObj($obj)
+	{
+		if (!$obj) {
+			return -1;
+		}
+
+		$this->id = $obj->rowid;
+		$this->fk_product_attribute_combination = doubleval($obj->fk_product_attribute_combination);
+		$this->fk_price_level = intval($obj->fk_price_level);
+		$this->variation_price = doubleval($obj->variation_price);
+		$this->variation_price_percentage = (bool) $obj->variation_price_percentage;
+
+		return 1;
+	}
+
+
+	/**
+	 * save
+	 *
+	 * @return int <0 KO, >0 OK
+	 */
+	public function save()
+	{
+		$errors = 0;
+
+
+		if (empty($this->fk_product_attribute_combination) || empty($this->fk_price_level)){
+			return -1;
+		}
+
+		// check if level exist in DB before add
+		if (empty($this->id)){
+			$sql = "SELECT rowid id"
+				." FROM ".MAIN_DB_PREFIX . $this->table_element
+				." WHERE fk_product_attribute_combination = ".(int) $this->fk_product_attribute_combination
+				.' AND fk_price_level = '.intval($this->fk_price_level);
+
+			$existObj = $this->db->getRow($sql);
+			if ($existObj){
+				$this->id = $existObj->id;
+			}
+		}
+
+		// Update
+		if (!empty($this->id)) {
+			$sql = 'UPDATE ' . MAIN_DB_PREFIX . $this->table_element . ' SET '
+				. ' variation_price = '.doubleval($this->variation_price)
+				. ' , variation_price_percentage = '.intval($this->variation_price_percentage)
+				. ' WHERE rowid = '.intval($this->id);
+
+			$res = $this->db->query($sql);
+			if ($res>0){
+				return $this->id;
+			}
+			else {
+				$this->error = $this->db->error();
+				$this->errors[] = $this->error;
+				return -1;
+			}
+		}
+		else {
+			// ADD
+			$sql = "INSERT INTO " . MAIN_DB_PREFIX . $this->table_element . " ("
+				. " fk_product_attribute_combination, fk_price_level, variation_price, variation_price_percentage"
+				. " ) VALUES ( "
+				. intval($this->fk_product_attribute_combination)
+				. ' , '.intval($this->fk_price_level)
+				. ' , '.doubleval($this->variation_price)
+				. ' , '.intval($this->variation_price_percentage)
+				. " )";
+
+			$res = $this->db->query($sql);
+			if ($res){
+				$this->id = $this->db->last_insert_id(MAIN_DB_PREFIX . $this->table_element);
+			}
+			else {
+				$this->error = $this->db->error();
+				$this->errors[] = $this->error;
+				return -1;
+			}
+		}
+
+		return $this->id;
+	}
+
+
+	/**
+	 * delete
+	 *
+	 * @return int <0 KO, >0 OK
+	 */
+	public function delete()
+	{
+		$res = $this->db->query("DELETE FROM ".MAIN_DB_PREFIX.$this->table_element
+			." WHERE rowid = ".(int) $this->id);
+
+		return $res ? 1 : -1;
+	}
+
+
+	/**
+	 * delete all for a combination
+	 *
+	 * @param $fk_product_attribute_combination
+	 * @return int <0 KO, >0 OK
+	 */
+	public function deleteAllForCombination($fk_product_attribute_combination)
+	{
+		$res = $this->db->query("DELETE FROM ".MAIN_DB_PREFIX.$this->table_element
+			." WHERE fk_product_attribute_combination = ".(int) $fk_product_attribute_combination);
+
+		return $res ? 1 : -1;
+	}
+
+
+	/**
+	 * Clean not needed price levels for a combination
+	 *
+	 * @param $fk_product_attribute_combination
+	 * @return int <0 KO, >0 OK
+	 */
+	public function clean($fk_product_attribute_combination)
+	{
+		global $conf;
+
+		$res = $this->db->query("DELETE FROM ".MAIN_DB_PREFIX.$this->table_element
+			. " WHERE fk_product_attribute_combination = ".(int) $fk_product_attribute_combination
+			. " AND fk_price_level > ".intval($conf->global->PRODUIT_MULTIPRICES_LIMIT) );
+
+
+		return $res ? 1 : -1;
+	}
+}

+ 89 - 6
htdocs/variants/combinations.php

@@ -33,6 +33,10 @@ $ref = GETPOST('ref', 'alpha');
 $weight_impact = GETPOST('weight_impact', 'alpha');
 $price_impact = GETPOST('price_impact', 'alpha');
 $price_impact_percent = (bool) GETPOST('price_impact_percent');
+
+$level_price_impact = GETPOST('level_price_impact', 'array');
+$level_price_impact_percent = GETPOST('level_price_impact_percent', 'array');
+
 $reference = GETPOST('reference', 'alpha');
 $form = new Form($db);
 
@@ -112,6 +116,18 @@ if ($_POST) {
 		    }
 			$weight_impact = price2num($weight_impact);
 			$price_impact = price2num($price_impact);
+
+			// for conf PRODUIT_MULTIPRICES
+			if ($conf->global->PRODUIT_MULTIPRICES) {
+				$level_price_impact = array_map('price2num', $level_price_impact);
+				$level_price_impact_percent = array_map('price2num', $level_price_impact_percent);
+			}
+			else {
+				$level_price_impact = array(1 => $weight_impact);
+				$level_price_impact_percent = array(1 => $price_impact_percent);
+			}
+
+
 			$sanit_features = array();
 
 			//First, sanitize
@@ -141,11 +157,10 @@ if ($_POST) {
 			// sanit_feature is an array with 1 (and only 1) value per attribute.
 			// For example:  Color->blue, Size->Small, Option->2
 			//var_dump($sanit_features);
-			//var_dump($productCombination2ValuePairs1); exit;
 
 			if (!$prodcomb->fetchByProductCombination2ValuePairs($id, $sanit_features))
 			{
-				$result = $prodcomb->createProductCombination($user, $object, $sanit_features, array(), $price_impact_percent, $price_impact, $weight_impact, $reference);
+				$result = $prodcomb->createProductCombination($user, $object, $sanit_features, array(), $level_price_impact_percent, $level_price_impact, $weight_impact, $reference);
 				if ($result > 0)
 				{
 					setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
@@ -227,6 +242,32 @@ if ($_POST) {
 		$prodcomb->variation_price = $price_impact;
 		$prodcomb->variation_weight = $weight_impact;
 
+		// for conf PRODUIT_MULTIPRICES
+		if ($conf->global->PRODUIT_MULTIPRICES) {
+			$level_price_impact = array_map('price2num', $level_price_impact);
+			$level_price_impact_percent = array_map(function ($a) {
+				return !empty($a);}, $level_price_impact_percent);
+
+			$prodcomb->variation_price = $level_price_impact[1];
+			$prodcomb->variation_price_percentage = (bool) $level_price_impact_percent[1];
+		}
+		else {
+			$level_price_impact = array(1 => $weight_impact);
+			$level_price_impact_percent = array(1 => $price_impact_percent);
+		}
+
+		if ($conf->global->PRODUIT_MULTIPRICES){
+			$prodcomb->combination_price_levels = array();
+			for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++){
+				$productCombinationLevel = new ProductCombinationLevel($db);
+				$productCombinationLevel->fk_product_attribute_combination = $prodcomb->id;
+				$productCombinationLevel->fk_price_level = $i;
+				$productCombinationLevel->variation_price = $level_price_impact[$i];
+				$productCombinationLevel->variation_price_percentage = $level_price_impact_percent[$i];
+				$prodcomb->combination_price_levels[$i] = $productCombinationLevel;
+			}
+		}
+
 		if ($prodcomb->update($user) > 0) {
 			setEventMessages($langs->trans('RecordSaved'), null, 'mesgs');
 			header('Location: '.dol_buildpath('/variants/combinations.php?id='.$id, 2));
@@ -594,12 +635,33 @@ if (!empty($id) || !empty($ref))
 				<td><label for="reference"><?php echo $langs->trans('Reference') ?></label></td>
 				<td><input type="text" id="reference" name="reference" value="<?php echo trim($reference) ?>"></td>
 			</tr>
+			<?php 		if (empty($conf->global->PRODUIT_MULTIPRICES)){ ?>
 			<tr>
 				<td><label for="price_impact"><?php echo $langs->trans('PriceImpact') ?></label></td>
 				<td><input type="text" id="price_impact" name="price_impact" value="<?php echo price($price_impact) ?>">
-				<input type="checkbox" id="price_impact_percent" name="price_impact_percent" <?php echo $price_impact_percent ? ' checked' : '' ?>> <label for="price_impact_percent"><?php echo $langs->trans('PercentageVariation') ?></label></td>
+				<input type="checkbox" id="price_impact_percent" name="price_impact_percent" <?php echo $price_impact_percent ? ' checked' : '' ?>> <label for="price_impact_percent"><?php echo $langs->trans('PercentageVariation') ?></label>
+				</td>
 			</tr>
-			<?php
+			<?php 		}
+			else {
+				$prodcomb->fetchCombinationPriceLevels();
+
+				for ($i = 1; $i <= $conf->global->PRODUIT_MULTIPRICES_LIMIT; $i++)
+				{
+					print '<tr>';
+					print '<td><label for="level_price_impact_'.$i.'">'.$langs->trans('ImpactOnPriceLevel', $i).'</label>';
+					if ($i===1){
+						print ' <a id="apply-price-impact-to-all-level" class="classfortooltip" href="#" title="'.$langs->trans('ApplyToAllPriceImpactLevelHelp').'">('.$langs->trans('ApplyToAllPriceImpactLevel').')</a>';
+					}
+					print '</td>';
+					print '<td><input type="text" class="level_price_impact" id="level_price_impact_'.$i.'" name="level_price_impact['.$i.']" value="'.price($prodcomb->combination_price_levels[$i]->variation_price).'">';
+					print '<input type="checkbox" class="level_price_impact_percent" id="level_price_impact_percent_'.$i.'" name="level_price_impact_percent['.$i.']" '. (!empty($prodcomb->combination_price_levels[$i]->variation_price_percentage) ? ' checked' : '' ).'> <label for="level_price_impact_percent_'.$i.'">'.$langs->trans('PercentageVariation').'</label>';
+
+					print '</td>';
+					print '</tr>';
+				}
+			}
+
             if ($object->isProduct()) {
 				print '<tr>';
 				print '<td><label for="weight_impact">'.$langs->trans('WeightImpact').'</label></td>';
@@ -609,9 +671,30 @@ if (!empty($id) || !empty($ref))
 			print '</table>';
 		}
 
-		dol_fiche_end();
-        ?>
+		if (!empty($conf->global->PRODUIT_MULTIPRICES)){
+			?>
+		<script>
+			$(document).ready(function() {
+				// Apply level 1 impact to all prices impact levels
+				$('body').on('click', '#apply-price-impact-to-all-level', function(e) {
+					e.preventDefault();
+					let priceImpact = $( "#level_price_impact_1" ).val();
+					let priceImpactPrecent = $( "#level_price_impact_percent_1" ).prop("checked");
+
+					var multipricelimit = <?php print intval($conf->global->PRODUIT_MULTIPRICES_LIMIT); ?>
+
+					for (let i = 2; i <= multipricelimit; i++) {
+						$( "#level_price_impact_" + i ).val(priceImpact);
+						$( "#level_price_impact_percent_" + i  ).prop("checked", priceImpactPrecent);
+					}
+				});
+			});
+		</script>
+			<?php
+		}
 
+		dol_fiche_end();
+		?>
 		<div style="text-align: center">
 		<input type="submit" name="create" <?php if (!is_array($productCombination2ValuePairs1)) print ' disabled="disabled"'; ?> value="<?php echo $action == 'add' ? $langs->trans('Create') : $langs->trans('Save') ?>" class="button">
 		&nbsp;