Browse Source

Retrieve processes from SFYCustom module

Mathieu Moulin 1 year ago
parent
commit
9fcdc26199

+ 35 - 1
admin/setup.php

@@ -26,4 +26,38 @@
 require_once '../env.inc.php';
 require_once '../main_load.inc.php';
 
-require_once('../../mmicommon/admin/mmisetup_1.inc.php');
+$arrayofparameters = array(
+	'SFYCUSTOM_FIELD_CLIENT_PRO'=>array('type'=>'yesno','enabled'=>1),
+
+	'MMI_ORDER_1CLIC_INVOICE_SHIPPING'=>array('type'=>'yesno','enabled'=>1),
+	'MMI_ORDER_1CLIC_INVOICE_SHIPPING_AUTOCLOSE'=>array('type'=>'yesno','enabled'=>1),
+	'MMI_ORDER_1CLIC_INVOICE'=>array('type'=>'yesno','enabled'=>1),
+	'MMI_ORDER_1CLIC_INVOICE_DELAY'=>array('type'=>'yesno','enabled'=>1),
+	'MMI_ORDER_1CLIC_INVOICE_EMAIL_AUTO'=>array('type'=>'yesno','enabled'=>1),
+	'MMI_ORDER_1CLIC_INVOICE_EMAIL_AUTO_NOPRO'=>array('type'=>'yesno','enabled'=>1),
+	'MMI_INVOICE_EMAILSEND'=>array('type'=>'yesno','enabled'=>1),
+
+	'SFYCUSTOM_LOCK'=>array('type'=>'yesno', 'enabled'=>1),
+	'MMI_ORDER_DRAFTIFY'=>array('type'=>'yesno', 'enabled'=>1),
+
+	'SHIPPING_PDF_HIDE_WEIGHT_AND_VOLUME'=>array('type'=>'yesno','enabled'=>1),
+	'SHIPPING_PDF_HIDE_BATCH'=>array('type'=>'yesno','enabled'=>1), // MMI Hack
+	'SHIPPING_PDF_HIDE_DELIVERY_DATE'=>array('type'=>'yesno','enabled'=>1), // MMI Hack
+	'SHIPPING_PDF_ANTIGASPI'=>array('type'=>'yesno','enabled'=>1), // MMI Hack
+	'MAIN_GENERATE_SHIPMENT_WITH_PICTURE'=>array('type'=>'yesno','enabled'=>1),
+	'MMI_SHIPPING_PDF_MESSAGE'=>array('type'=>'html','enabled'=>1),
+
+	'MMI_INVOICE_DRAFTIFY'=>array('type'=>'yesno', 'enabled'=>1),
+	'MMI_INVOICE_DRAFTIFY_TYPES'=>array('type'=>'string', 'enabled'=>1),
+
+	'MMI_1CT_FIX'=>array('type'=>'yesno', 'enabled'=>1),
+	'MMI_1CT_DIFFLIMIT'=>array('type'=>'decimal', 'enabled'=>1, 'default'=>0.03),
+	'MMI_VAT_TX_FIX'=>array('type'=>'yesno', 'enabled'=>1),
+
+	'SFY_ALERT_ORDER_NOT_SHIPPED'=>array('type'=>'yesno', 'enabled'=>1),
+
+	'MMI_MOVEMENT_LIST_ENHANCE'=>array('type'=>'yesno', 'enabled'=>1),
+);
+
+require_once '../../mmicommon/admin/mmisetup_1.inc.php';
+

+ 348 - 9
class/actions_mmiworkflow.class.php

@@ -1,25 +1,364 @@
 <?php
-
-/**
- * Copyright © 2015-2016 Marcos García de La Fuente <hola@marcosgdf.com>
- *
- * This file is part of Multismtp.
+/* Copyright (C) 2022-2024 Mathieu Moulin <mathieu@iprospective.fr>
  *
- * Multismtp is free software: you can redistribute it and/or modify
+ * 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.
  *
- * Multismtp is distributed in the hope that it will be useful,
+ * 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 Multismtp.  If not, see <http://www.gnu.org/licenses/>.
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * \file    mmiworkflow/class/actions_mmiworkflow.class.php
+ * \ingroup mmiworkflow
+ * \brief   Example hook overload.
+ *
+ * Put detailed description here.
  */
 
-class ActionsMMIWorkflow
+require_once DOL_DOCUMENT_ROOT.'/commande/class/commande.class.php';
+dol_include_once('custom/mmicommon/class/mmi_actions.class.php');
+dol_include_once('custom/mmiworkflow/class/mmi_workflow.class.php');
+
+
+class ActionsMMIWorkflow extends MMI_Actions_1_0
 {
+	const MOD_NAME = 'mmiworkflow';
+
+	/**
+	 * Overloading the addMoreMassActions function : replacing the parent's function with the one below
+	 *
+	 * @param   array           $parameters     Hook metadatas (context, etc...)
+	 * @param   CommonObject    $object         The object to process (an invoice if you are in invoice module, a propale in propale's module, etc...)
+	 * @param   string          $action         Current action (if set). Generally create or edit or null
+	 * @param   HookManager     $hookmanager    Hook manager propagated to allow calling another hook
+	 * @return  int                             < 0 on error, 0 on success, 1 to replace standard code
+	 */
+	public function addMoreActionsButtons($parameters, &$object, &$action, $hookmanager)
+	{
+		global $conf, $user, $langs;
+
+		$error = 0; // Error counter
+		$disabled = 1;
+
+		// Fiche Commande
+		if ($this->in_context($parameters, 'ordercard')) {
+			/** @var Commande $object */
+			// Blocage validation si ligne libre avec produit (pas grave en service)
+			if (!empty($conf->global->SFYCUSTOM_LOCK)) {
+				$blockValid='';
+				foreach($object->lines as $line) {
+					if (empty($line->fk_product) && $line->product_type == 0 && $line->qty>0) {
+						$blockValid = $line->description;
+						break;
+					}
+				}
+
+				if (!empty($blockValid)) {
+					print '
+					<script type="text/javascript">
+						$(document).ready(function() {
+							$(\'a.butAction[href*="action=validate"\').removeClass(\'butAction\')
+							.addClass(\'butActionRefused\').attr(\'href\',\'#\').text(\''.$langs->transnoentities('SfCstCannotValidWithFreeLine').'\')
+							.attr(\'title\',\''.dol_string_nohtmltag($blockValid).'\');
+						})
+					</script>';
+				}
+			}
+			// Ajout action Facture & Expé en 1 Clic
+			// SSI statut commande = validé
+			//var_dump($object);
+			if (!empty($conf->global->MMI_ORDER_1CLIC_INVOICE_SHIPPING) && (int)$object->status===Commande::STATUS_VALIDATED) {
+				$link = '?id='.$object->id.'&action=1clic_invoice_shipping';
+				// Test si déjà facture
+				// Test si déjà expédition
+
+				echo "<a class='butAction' href='".$link."' onclick='return confirm(\"".addslashes($langs->trans("MMI1ClickOrderInvoiceShippingConfirm"))."\")'>".$langs->trans("MMI1ClickOrderInvoiceShipping")."</a>";
+			}
+			// Bouton remise en brouillon
+			if (!empty($conf->global->MMI_ORDER_DRAFTIFY) && $user->rights->mmiworkflow->commande->draftify) {
+				$q = $this->db->query("SELECT 1
+					FROM ".MAIN_DB_PREFIX."commande c
+					WHERE c.rowid=".$object->id." AND c.fk_statut > 0");
+				//var_dump($q);
+				if ($q->num_rows) {
+					$link = '?id='.$object->id.'&action=draftify';
+					echo "<a class='butAction' href='".$link."'>".$langs->trans("MMIInvoiceDraftify")."</a>";
+				}
+			}
+			// Fix bug 1ct Presta & co
+			if ($conf->global->MMI_1CT_FIX) {
+				$link = '?id='.$object->id.'&action=1ct_fix';
+				echo "<a class='butAction' href='".$link."'>".$langs->trans("MMI1ctFix")."</a>";
+			}
+			// Fix bug TVA Presta & co
+			if ($conf->global->MMI_VAT_TX_FIX) {
+				$link = '?id='.$object->id.'&action=vat_tx_fix';
+				echo "<a class='butAction' href='".$link."'>".$langs->trans("MMIVATTxFix")."</a>";
+			}
+		}
+		// Fiche Facture
+		elseif ($this->in_context($parameters, 'invoicecard')) {
+			/** @var Facture $object */
+			$q = $this->db->query("SELECT 1
+				FROM ".MAIN_DB_PREFIX."facture i
+				LEFT JOIN ".MAIN_DB_PREFIX."accounting_bookkeeping j ON j.doc_type='customer_invoice' AND j.fk_doc=i.rowid
+				WHERE i.rowid=".$object->id." AND i.fk_statut > 0 AND j.rowid IS NULL");
+			//var_dump($q);
+			$nocompta = ($q->num_rows);
+
+			// Bouton remise en brouillon
+			if ($conf->global->MMI_INVOICE_DRAFTIFY && $user->rights->mmiworkflow->facture->draftify) {
+				if (!isset($conf->global->MMI_INVOICE_DRAFTIFY_TYPES) || $conf->global->MMI_INVOICE_DRAFTIFY_TYPES=='' || in_array($object->type, explode(',', $conf->global->MMI_INVOICE_DRAFTIFY_TYPES))) {
+					if ($nocompta) {
+						$link = '?facid='.$object->id.'&action=draftify';
+						echo "<a class='butAction' href='".$link."'>".$langs->trans("MMIInvoiceDraftify")."</a>";
+					}
+					else {
+						echo '<a class="butActionRefused" title="'.$langs->trans('DisabledBecauseDispatchedInBookkeeping').'" href="javascript:;">'.$langs->trans("MMIInvoiceDraftify").'</a>';
+					}
+				}
+				else {
+					echo '<a class="butActionRefused" title="'.$langs->trans('NotAllowed').'" href="javascript:;">'.$langs->trans("MMIInvoiceDraftify").'</a>';
+				}
+			}
+			// Fix bug 1ct Presta & co
+			if ($conf->global->MMI_1CT_FIX) {
+				if ($nocompta) {
+					$link = '?facid='.$object->id.'&action=1ct_fix';
+					echo "<a class='butAction' href='".$link."'>".$langs->trans("MMI1ctFix")."</a>";
+				}
+				else {
+					echo '<a class="butActionRefused" title="'.$langs->trans('DisabledBecauseDispatchedInBookkeeping').'" href="javascript:;">'.$langs->trans("MMI1ctFix")."</a>";
+				}
+			}
+			// Fix bug TVA Presta & co
+			if ($conf->global->MMI_VAT_TX_FIX) {
+				$link = '?id='.$object->id.'&action=vat_tx_fix';
+				echo "<a class='butAction' href='".$link."'>".$langs->trans("MMIVATTxFix")."</a>";
+			}
+			// Envoi facture par email
+			if ($conf->global->MMI_INVOICE_EMAILSEND) {
+				$link = '?facid='.$object->id.'&action=email_send';
+				echo "<a class='butAction' href='".$link."'>".$langs->trans("MMIInvoiceEmailSend")."</a>";
+			}
+		}
+
+		if (!$error) {
+			return 0; // or return 1 to replace standard code
+		} else {
+			$this->errors[] = 'Error message';
+			return -1;
+		}
+	}
+
+	function doActions($parameters, &$object, &$action, $hookmanager)
+	{
+		global $conf, $user, $langs;
+
+		$error = 0; // Error counter
+		$disabled = 1;
+
+		// Commande
+		if ($this->in_context($parameters, 'ordercard')) {
+			// 1 click invoice shipping
+			if ($action === '1clic_invoice_shipping') {
+				if ($conf->global->MMI_ORDER_1CLIC_INVOICE_SHIPPING) {
+					mmi_workflow::order_1clic_invoice_shipping($user, $object);
+				}
+			}
+			// Indraft again
+			if ($action === 'draftify') {
+				if ($conf->global->MMI_ORDER_DRAFTIFY && $user->rights->mmiworkflow->commande->draftify) {
+					// @todo check pas envoyée au client !
+					$sql = "UPDATE ".MAIN_DB_PREFIX."commande c
+						SET c.fk_statut=0
+						WHERE c.rowid=".$object->id;
+					//var_dump($object);
+					//die($sql);
+					$this->db->query($sql);
+				}
+			}
+			// Fix bug 1ct Presta & co
+			if ($action === '1ct_fix') {
+				if ($conf->global->MMI_1CT_FIX) {
+					mmi_workflow::order_1ctfix($user, $object);
+				}
+			}
+			// Fix bug 1ct Presta & co
+			if ($action === '1ct_fix') {
+				if ($conf->global->MMI_1CT_FIX) {
+					mmi_workflow::order_1ctfix($user, $object);
+				}
+			}
+			// Fix bug TVA Presta & co
+			if ($action === 'vat_tx_fix') {
+				if ($conf->global->MMI_VAT_TX_FIX) {
+					mmi_workflow::order_vat_tx_fix($user, $object);
+				}
+			}
+		}
+		// Invoice
+		elseif ($this->in_context($parameters, 'invoicecard')) {
+			// Indraft again
+			if ($action === 'draftify') {
+				if ($conf->global->MMI_INVOICE_DRAFTIFY && $user->rights->mmiworkflow->facture->draftify) {
+					// @todo check pas envoyée au client !
+					mmi_workflow::invoice_draftify($user, $object);
+				}
+			}
+			// Email send
+			if ($action === 'email_send') {
+				if ($conf->global->MMI_INVOICE_EMAILSEND) {
+					mmi_workflow::invoice_email($user, $object);
+				}
+			}
+			// Fix bug 1ct Presta & co
+			if ($action === '1ct_fix') {
+				if ($conf->global->MMI_1CT_FIX) {
+					mmi_workflow::invoice_1ctfix($user, $object);
+				}
+			}
+			// Fix bug TVA Presta & co
+			if ($action === 'vat_tx_fix') {
+				if ($conf->global->MMI_VAT_TX_FIX) {
+					mmi_workflow::invoice_vat_tx_fix($user, $object);
+				}
+			}
+		}
+		// Shippement
+		if ($this->in_context($parameters, 'shipmentlist') && !empty($conf->global->SFY_ALERT_ORDER_NOT_SHIPPED)) {
+			$order = new Commande($this->db);
+			$sql="SELECT count(rowid) as cnt FROM ".MAIN_DB_PREFIX."commande where fk_statut=".$order::STATUS_VALIDATED;
+			$resql = $this->db->query($sql);
+			if (!$resql) {
+				setEventMessage($this->db->error,'errors');
+			} else {
+				$obj=$this->db->fetch_object($resql);
+				if ($obj->cnt>0) {
+					$urlList = dol_buildpath('/commande/list.php',2).'?search_status=1';
+					$urlList='<a href="'.$urlList.'">'.$langs->trans('List').'</a>';
+					setEventMessage($langs->transnoentities('OrderInValidatedStatusAlert',$urlList),'warnings');
+				}
+			}
+		}
+
+		if (!$error) {
+			return 0; // or return 1 to replace standard code
+		} else {
+			$this->errors[] = 'Error message';
+			return -1;
+		}
+	}
 
+	function beforePDFCreation($parameters, &$object, &$action='', $hookmanager=NULL)
+	{
+		global $conf, $user, $langs;
+
+		$error = 0; // Error counter
+		$disabled = 1;
+
+		if (!is_object($object))
+			return -1;
+
+		$object_type = get_class($object);
+		//var_dump($parameters); die();
+		//var_dump($this->in_context($parameters, 'pdfgeneration'), $object_type);
+		if ($this->in_context($parameters, 'pdfgeneration') && $object_type=='Expedition') {
+			// Sort by Product Ref
+			// -> directement dans le coeur ça merde l'association avec les images sinon
+			//usort($object->lines, function($i, $j){
+			//	return ($i->ref > $j->ref) ?1 :(($i->ref < $j->ref) ?-1 :0);
+			//});
+			// Bonbons
+			if (!empty($conf->global->MMI_SHIPPING_PDF_MESSAGE)) {
+				// Shipping message
+				if (!empty($conf->global->MMI_SHIPPING_PDF_MESSAGE))
+					$object->note_public .= (!empty($object->note_public) ?'<br />' :'').$conf->global->MMI_SHIPPING_PDF_MESSAGE.'<br />';
+			}
+		}
+
+		if (!$error) {
+			return 0; // or return 1 to replace standard code
+		} else {
+			$this->errors[] = 'Error message';
+			return -1;
+		}
+	}
+
+	function pdf_writelinedesc($parameters, &$object, &$action='', $hookmanager=NULL)
+	{
+		global $conf, $user, $langs;
+
+		$error = 0; // Error counter
+		$disabled = 1;
+
+		if (!is_object($object))
+			return -1;
+
+		$object_type = get_class($object);
+		//var_dump($object_type); die();
+
+		if ($this->in_context($parameters, 'pdfgeneration') && $object_type=='Expedition') {
+			$i = $parameters['i'];
+			//$object = $parameters['object'];
+			$object->lines[$i]->ref = '<b>'.$object->lines[$i]->ref.'</b>';
+			$object->lines[$i]->product_ref = '<b>'.$object->lines[$i]->product_ref.'</b>';
+			$object->lines[$i]->label = '<b>'.$object->lines[$i]->label.'</b>';
+			//var_dump($object->lines[$i]->label); die();
+			//var_dump($object->lines[$i]); die();
+			//var_dump('yo'); die();
+			//$this->resPrint = '<b>'.$object->lines[$i]->ref.'</b>';
+		}
+
+		//$this->resprints = ' - TOTO';
+
+		if (!$error) {
+			return 0; // or return 1 to replace standard code
+		} else {
+			$this->errors[] = 'Error message';
+			return -1;
+		}
+	}
+
+	function pdf_build_address($parameters, &$object, &$action='', $hookmanager=NULL)
+	{
+		global $conf, $user, $langs;
+
+		$error = 0; // Error counter
+		$disabled = 1;
+
+		if (!is_object($object))
+			return -1;
+
+		$object_type = get_class($object);
+
+		if ($this->in_context($parameters, 'pdfgeneration') && $object_type=='Expedition' && $parameters['mode']=='targetwithdetails') {
+			$arrayidcontact = $object->commande->getIdContact('external', 'SHIPPING');
+			if (count($arrayidcontact) > 0) {
+				$object->fetch_contact($arrayidcontact[0]);
+				if (!empty($object->contact->array_options['options_p_company']))
+					$this->resprints = $object->contact->array_options['options_p_company'];
+			}
+		}
+
+		//$this->resprints = ' - TOTO';
+
+		if (!$error) {
+			return 0; // or return 1 to replace standard code
+		} else {
+			$this->errors[] = 'Error message';
+			return -1;
+		}
+	}
 }
+
+ActionsMMIWorkflow::__init();
+

+ 789 - 0
class/mmi_workflow.class.php

@@ -0,0 +1,789 @@
+<?php
+
+require_once DOL_DOCUMENT_ROOT.'/compta/facture/class/facture.class.php';
+require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php';
+//require_once DOL_DOCUMENT_ROOT.'/expedition/class/expeditionbatch.class.php';
+
+dol_include_once('custom/mmicommon/class/mmi_generic.class.php');
+// @todo check if module
+dol_include_once('/custom/mmipayments/class/mmi_payments.class.php');
+
+class mmi_workflow extends mmi_generic_1_0
+{
+	const MOD_NAME = 'mmiworkflow';
+
+	public static function order_1clic_invoice($user, $order, $validate=true)
+	{
+		if (! $user->rights->facture->creer)
+			return;
+		
+		//var_dump($object);
+		global  $conf, $langs, $db, $soc;
+
+		$invoice = new Facture($db);
+		$invoice_gen = true;
+
+		// Vérif si pas déjà une facture !
+		$order->fetchObjectLinked();
+		//var_dump($order->linkedObjectsIds); die();
+		if(!empty($order->linkedObjectsIds['facture'])) {
+			foreach($order->linkedObjectsIds['facture'] as $id) {
+				$invoice->fetch($id);
+				break;
+			}
+			// @todo check if already
+			//$invoice_gen = false;
+		}
+		else {
+			$invoice->createFromOrder($order, $user);
+			// Validation
+			if ($validate)
+				$invoice->validate($user);
+			// Assign payments
+			// @todo c'est degue je dois tester l'activation du module !!
+			if (class_exists('mmi_payments')) {
+				mmi_payments::invoice_autoassign_payments($invoice);
+			}
+			// Retrieve everything again
+			$invoice->fetch($invoice->id);
+		}
+
+		// Fix bug
+		static::invoice_1ctfix($user, $invoice);
+		static::order_1ctfix($user, $order);
+
+		// Parfois ça ne classe pas correctement si on a un écart de centime par exemple
+		if (!$order->billed)
+			$order->classifyBilled($user); // On est sur du 1clic !
+
+		// Génération PDF
+		if ($invoice_gen) {
+			// @todo : dégueu
+			if (file_exists($filename=DOL_DATA_ROOT.'/doctemplates/invoices/CALICOTE_FACTURE.odt'))
+				$docmodel = 'generic_invoice_odt:'.$filename;
+			elseif (file_exists($filename=DOL_DATA_ROOT.'/doctemplates/invoices/PISCEEN_FACTURE.odt'))
+				$docmodel = 'generic_invoice_odt:'.$filename;
+			else
+				$docmodel = $invoice->model_pdf;
+			$hidedetails = 0;
+			$hidedesc = 0;
+			$hideref = 0;
+			$moreparams = null;
+			$invoice->generateDocument($docmodel, $langs, $hidedetails, $hidedesc, $hideref, $moreparams);
+		}
+
+		// Send invoice by email if Option true, not already sent and invoice validated correctly (closes & paid) !
+		// STOP if not auto send
+		if (empty($conf->global->MMI_ORDER_1CLIC_INVOICE_EMAIL_AUTO))
+			return;
+
+		// Customer
+		$thirdparty = $invoice->thirdparty;
+
+		// if specified not to send auto (thirdparty option)
+		// OR specified not to send auto to pro (module option + thirdparty check pro field)
+		if (
+			!empty($thirdparty->array_options['options_invoice_noautosend'])
+			|| (!empty($conf->global->MMI_ORDER_1CLIC_INVOICE_EMAIL_AUTO_NOPRO) && !empty($thirdparty->array_options['options_pro']))
+			)
+			return;
+
+		// Check already sent
+		$sql = 'SELECT 1
+			FROM '.MAIN_DB_PREFIX.'actioncomm a
+			WHERE a.code="AC_BILL_SENTBYMAIL" AND a.elementtype="invoice" AND a.fk_element='.$invoice->id.'
+			LIMIT 1';
+		//echo $sql;
+		$q = $db->query($sql);
+		//var_dump($q); //die();
+		$invoice_sent = $q->num_rows>0;
+		//var_dump($invoice_sent);
+		if ($invoice_sent)
+			return;
+
+		// Check closed (sent), and payed if not professionnal
+		$validated = (empty($thirdparty->array_options['options_pro']) && $invoice->statut == Facture::STATUS_CLOSED)
+			|| (!empty($thirdparty->array_options['options_pro']) && in_array($invoice->statut, [Facture::STATUS_VALIDATED, Facture::STATUS_CLOSED]));
+		//var_dump($validated, $invoice); die();
+		if (!$validated)
+			return;
+
+		static::invoice_email($user, $invoice);
+
+		return true;
+	}
+
+	public static function order_1ctfix($user, $object, $autovalidate=true)
+	{
+		global $conf, $langs, $db, $soc, $hookmanager;
+
+		$sql = 'SELECT SUM(ip.amount) paid
+			FROM '.MAIN_DB_PREFIX.'paiement_object ip
+			WHERE ip.fk_object='.$object->id;
+		$q2 = $db->query($sql);
+		if (!$q2 || !($row = $q2->fetch_object()))
+			return;
+		//var_dump($row->paid);
+
+		$difflimit = !empty($conf->global->MMI_1CT_DIFFLIMIT) ?$conf->global->MMI_1CT_DIFFLIMIT :0.03;
+		$diff = $object->total_ttc-$row->paid;
+		$diffround = round($diff, 2);
+		$diffabs = abs($diffround);
+		//var_dump($diffabs); die();
+
+		// Marge de tolérance
+		if ($diffabs == 0 || $diffabs > $difflimit)
+			return;
+		
+		$statut = $object->statut;
+		if ($statut>0)
+			$object->setDraft($user);
+		foreach($object->lines as $line) if (($line->subprice > 0 || $line->subprice < 0) && $line->qty > 0) {
+			// On ajuste le subprice pour tomber pile poil sans avoir à faire de modif
+			$subprice = $line->subprice - ($diff>=1 ?$diff-0.5 :($diff<=-1 ?$diff-0.5 :$diff))/$line->qty/(1+$line->tva_tx/100);
+			//var_dump($line->subprice, $subprice);
+			$r = $object->updateline($line->id, $line->desc, $subprice, $line->qty, $line->remise_percent, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, 'HT', $line->info_bits, $line->date_start, $line->date_end, $line->product_type, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->special_code, 0, $line->fk_unit, $subprice, 0, $line->ref_ext);
+
+			// On vérifie normalement on est bon du premier coup mais on sait jamais, donc on boucle sur les autre produits si jamais
+			$diff = $object->total_ttc-$row->paid;
+			if (round($diff, 2)==0)
+				break;
+		}
+
+		// On valide quel que soit le statut initial
+		if ($statut>=1 || $autovalidate) {
+			$object->valid($user);
+			if ($statut==Commande::STATUS_SHIPMENTONPROCESS) {
+				$sql = 'UPDATE '.MAIN_DB_PREFIX.'commande SET fk_statut='.Commande::STATUS_SHIPMENTONPROCESS.'
+					WHERE rowid='.$object->id;
+				$q2 = $db->query($sql);
+				if (!$q2) {
+					// @todo error
+				}
+			}
+			elseif($statut==Commande::STATUS_CLOSED) {
+				$object->cloture($user);
+			}
+		}
+		
+		// @todo vérif si vraiment utile...
+		$object->fetch($object->id);
+
+		return true;
+	}
+
+	public static function order_vat_tx_fix($user, $object)
+	{
+		global $conf, $langs, $db, $soc, $hookmanager;
+
+		// @todo...
+	}
+
+	public static function invoice_1ctfix($user, $object, $autovalidate=true)
+	{
+		global $conf, $langs, $db, $soc, $hookmanager;
+
+		// Si pas de paiement, rien à corriger...
+		$sql = 'SELECT SUM(ip.amount) paid
+			FROM '.MAIN_DB_PREFIX.'paiement_facture ip
+			WHERE ip.fk_facture='.$object->id;
+		$q2 = $db->query($sql);
+		if (!$q2 || !($row = $q2->fetch_object()))
+			return;
+		
+		//var_dump($row->paid, $order->total_ttc);
+		$difflimit = !empty($conf->global->MMI_1CT_DIFFLIMIT) ?$conf->global->MMI_1CT_DIFFLIMIT :0.03;
+		$diff = $object->total_ttc-$row->paid;
+		$diffround = round($diff, 2);
+		$diffabs = abs($diffround);
+		//var_dump($diffabs);
+
+		// Marge de tolérance
+		if ($diffabs == 0 || $diffabs > $difflimit)
+			return;
+		$statut = $object->statut;
+		if ($statut>0)
+			$object->setDraft($user);
+		// $line->subprice f*****g string should be a float
+		foreach($object->lines as $line) if (($line->subprice > 0 || $line->subprice < 0) && $line->qty > 0) {
+			//var_dump($line); die();
+			// On ajuste le subprice pour tomber pile poil sans avoir à faire de modif
+			$subprice = $line->subprice - ($diff>=1 ?$diff-0.5 :($diff<=-1 ?$diff-0.5 :$diff))/$line->qty/(1+$line->tva_tx/100);
+			//var_dump($line->subprice, $subprice);
+			$r = $object->updateline($line->id, $line->desc, $subprice, $line->qty, $line->remise_percent, $line->date_start, $line->date_end, $line->tva_tx, $line->localtax1_tx, $line->localtax2_tx, 'HT', $line->info_bits, $line->product_type, $line->fk_parent_line, 0, $line->fk_fournprice, $line->pa_ht, $line->label, $line->special_code, 0, 100, $line->fk_unit, $subprice, 0, $line->ref_ext);
+
+			// On vérifie normalement on est bon du premier coup mais on sait jamais, donc on boucle sur les autre produits si jamais
+			$diff = $object->total_ttc-$row->paid;
+			if (round($diff, 2)==0)
+				break;
+		}
+
+		// On valide quel que soit le statut initial
+		if ($statut>=1 || $autovalidate) {
+			$object->validate($user);
+			$diff = $object->total_ttc-$row->paid;
+			// @todo voir pourquoi ça ne passe pas parfois, peut-être nécessite de refaire un fetch
+			if (round($diff, 2)==0 && $object->statut<=1)
+				$object->setPaid($user);
+		}
+
+		// @todo vérif si vraiment utile...
+		$object->fetch($object->id);
+
+		return true;
+	}
+
+	public static function invoice_vat_tx_fix($user, $object)
+	{
+		global $conf, $langs, $db, $soc, $hookmanager;
+
+		// @todo...
+	}
+
+	public static function invoice_email($user, $invoice)
+	{
+		global $conf, $langs, $db, $soc, $hookmanager;
+
+		$massaction = 'confirm_presend';
+		$_POST['oneemailperrecipient'] = 'on';
+		$_POST['addmaindocfile'] = 'on';
+		$_POST['sendmail'] = 'on';
+		$objectclass = 'Facture';
+		$thirdparty = $invoice->thirdparty;
+		// @todo fetch associated order
+		$order = NULL;
+		//var_dump($thirdparty); die();
+
+		//var_dump($thirdparty); die();
+		//var_dump($thirdparty->nom); die();
+		$_POST['receiver'] = $thirdparty->nom.' <'.$thirdparty->email.'>';
+		//var_dump($_POST['receiver']); die();
+		$_POST['fromtype'] = 'company'; // @todo modifier ! mettre le responsable du client
+		$_POST['subject'] = 'Votre facture '.$conf->global->MAIN_INFO_SOCIETE_NOM;
+		$_POST['message'] = 'Bonjour '.$thirdparty->nom.",\r\n\r\n"
+			.'Veuillez trouver ci-joint la facture de votre dernier achat chez '.$conf->global->MAIN_INFO_SOCIETE_NOM.','."\r\n"
+			.'en espérant que vous serez satisfait de nos produits'."\r\n\r\n"
+			.($order ?'Réf Commande: '.$order->ref."\r\n\r\n" :'')
+			.'A bientôt !'."\r\n\r\n"
+			.'L\'équipe '.$conf->global->MAIN_INFO_SOCIETE_NOM."\r\n";
+		$toselect = [$invoice->id];
+		$uploaddir = DOL_DOCUMENT_ROOT.'/../documents/facture';
+		require DOL_DOCUMENT_ROOT.'/core/actions_massactions.inc.php';
+
+		// Update "emailsent" field
+		$sql = 'SELECT *
+			FROM '.MAIN_DB_PREFIX.'facture_extrafields
+				WHERE fk_object='.$invoice->id;
+		$q = $db->query($sql);
+		if ($q && ($row = $q->fetch_object())) {
+			if (!$row->emailsent) {
+				$sql = 'UPDATE '.MAIN_DB_PREFIX.'facture_extrafields
+					SET emailsent=1
+					WHERE fk_object='.$invoice->id;
+				$db->query($sql);
+			}
+		}
+		else {
+			$sql = 'INSERT INTO '.MAIN_DB_PREFIX.'facture_extrafields
+				(fk_object, emailsent)
+				VALUES
+				('.$invoice->id.', 1)';
+			$db->query($sql);
+		}
+		//var_dump($sql);
+	}
+
+	// Création expédition
+	public static function order_1clic_shipping($user, $order, $validate=true, $lots=[])
+	{
+		//var_dump($order); die();
+		//var_dump($user); die();
+		global $conf, $db, $langs;
+		
+		dol_syslog($langs->trans("mmi_workflow::order_1clic_shipping() ".$order->id.' : '.json_encode($lots)), LOG_NOTICE);
+
+		if (! $user->rights->expedition->creer)
+			return;
+		
+		// vérif statut
+		if ((int)$order->status !== Commande::STATUS_VALIDATED)
+			return;
+	
+		$order->fetchObjectLinked();
+		//var_dump($order->linkedObjectsIds); die();
+		if(!empty($order->linkedObjectsIds['shipping']))
+			return;
+		
+		$origin = 'commande';
+		$origin_id = $order->id;
+		$objectsrc = $order;
+		$date_delivery = date('Y-m-d');
+		$mode_pdf = '';
+
+		// Paramétrage du picking des lots
+		// 0 => par DDM par ancienneté (puis même dépôt)
+		// 1 => Même dépôt en premier puis DDM
+		// (prévoir le paramétrage d'un ordre des dépôts)
+		$batch_conf = 0;
+		// Paramétrage du picking
+		// 0 => Recherche dans tous les dépôts
+		// 1 => Recherche dans une liste de dépôts (et sous-dépôts)
+		// 2 => Recherche dans un dépôt (et sous-dépôts)
+		$warehouse_conf = 0;
+
+		// Default
+		$warehouse_ids = 1;
+
+		$error = 0;
+
+		$db->begin();
+
+		$object = new Expedition($db);
+		$extrafields = new ExtraFields($db);
+
+		$staticwarehouse = new Entrepot($db);
+		if ($warehouse_ids > 0) {
+			$staticwarehouse->fetch($warehouse_ids);
+		}
+		
+		$object->origin = $origin;
+		$object->origin_id = $origin_id;
+		$object->fk_project = $objectsrc->fk_project;
+
+		// $object->weight				= 0;
+		// $object->sizeH				= 0;
+		// $object->sizeW				= 0;
+		// $object->sizeS				= 0;
+		// $object->size_units = 0;
+		// $object->weight_units = 0;
+
+		$object->socid = $objectsrc->socid;
+		$object->ref_customer = $objectsrc->ref_client;
+		$object->model_pdf = $mode_pdf;
+		$object->date_delivery = $date_delivery; // Date delivery planed
+		$object->shipping_method_id	= $objectsrc->shipping_method_id;
+		$object->tracking_number = '';
+		$object->note_private = $objectsrc->note_private;
+		$object->note_public = $objectsrc->note_public;
+		$object->fk_incoterms = $objectsrc->fk_incoterms;
+		$object->location_incoterms = $objectsrc->location_incoterms;
+
+		$batch_line = array();
+		$stockLine = array();
+		$array_options = array();
+
+		$num = count($objectsrc->lines);
+		$totalqty = 0;
+
+		// Analyse des quantités concernant les lots demandés
+		$lots2 = [];
+		if (!empty($lots)) {
+			foreach($lots as &$lot) {
+				$sql = 'SELECT pl.fk_product, p.ref, p.label, pl.batch, pl.eatby, pl.sellby, SUM(IF(pb.qty>0,pb.qty,0)) qty
+					FROM '.MAIN_DB_PREFIX.'product_lot pl
+					LEFT JOIN '.MAIN_DB_PREFIX.'product p ON p.rowid=pl.fk_product 
+					LEFT JOIN '.MAIN_DB_PREFIX.'product_stock ps ON ps.fk_product=pl.fk_product 
+					LEFT JOIN '.MAIN_DB_PREFIX.'product_batch pb ON pb.batch=pl.batch AND pb.fk_product_stock=ps.rowid
+					WHERE pl.rowid = '.$lot['fk_product_lot'];
+				$q = $db->query($sql);
+				if($row=$q->fetch_assoc()) {
+					$lot = array_merge($lot, $row);
+					if (empty($lots2[$lot['fk_product']][$lot['batch']]))
+						$lots2[$lot['fk_product']][$lot['batch']] = $lot;
+					else
+						$lots2[$lot['fk_product']][$lot['batch']]['qte'] += $lot['qte'];
+
+					// Analyse erreur stock a priori
+					if ($lots2[$lot['fk_product']][$lot['batch']]['qte'] > $lots2[$lot['fk_product']][$lot['batch']]['qty']) {
+						setEventMessages($langs->trans("ErrorProductStockUnavailable", $lot['ref'].' '.$lot['label']), null, 'errors');
+						dol_syslog($langs->trans("ErrorProductStockUnavailable", $lot['ref'].' '.$lot['label']), LOG_ERR);
+						$error++;
+						break;
+					}
+				}
+			}
+		}
+		//var_dump($lots2);
+		
+		$sql = '';
+
+		// Parcours produits commande
+		if (!$error) for ($i = 0; $i < $num; $i++) {
+			$line = $objectsrc->lines[$i];
+			//var_dump($line->qty);
+
+			//var_dump($conf->productbatch->enabled, $line);
+			if (! $line->fk_product)
+				continue;
+
+			$product = new Product($db);
+			$product->fetch($line->fk_product);
+			if (! $product->id)
+				continue;
+			// Kit alimentaire => ne pas expédier ça bug
+			//if (!empty($product->array_options['options_compose']))
+			//	continue;
+
+			// Product shippable (not service, etc.)
+			if ($product->type != 0)
+				continue;
+
+			$product->load_stock('warehouseopen');
+			//var_dump($product->label);
+
+			// Extrafields
+			$array_options[$i] = $extrafields->getOptionalsFromPost($object->table_element_line, $i);
+			
+			// cbien de ce produit est dans l'expé
+			// (permet de savoir lorsqu'on s'arrête)
+			$subtotalqty = 0;
+
+			// Parcours entrepots
+			// @todo préférer $warehouse_id & enfants
+			if (!empty($conf->productbatch->enabled) && $line->product_tobatch) {      // If product need a batch number
+
+				// A construire : $batch_line[$i] qui sera utilisé via adlinebatch($batch_line[$i])
+				// c.f. expedition/card.php
+				$batch_line[$i] = [];
+				
+				$productlots = [];
+				// Détail des batchs ajoutés
+				$sub_qty = [];
+				
+				if ($batch_conf==0) {
+					// Premier parcours, tri par lot par DDM
+					foreach($product->stock_warehouse as $stock) {
+						foreach($stock->detail_batch as $batch=>$productbatch) {
+							if (!isset($productlots[$batch]))
+								$productlots[$batch] = $productbatch->sellby ?date('Y-m-d', $productbatch->sellby) :'';
+						}
+					}
+					asort($productlots);
+					//var_dump($productlots);
+
+					if (isset($lots2[$product->id])) {
+						foreach($lots2[$product->id] as $batch=>&$batch_detail) {
+							// Plus rien à prendre
+							if ($batch_detail['qte']<=0)
+								continue;
+							// Plus rien en stock
+							if ($batch_detail['qty']<=0) {
+								setEventMessages($langs->trans("ErrorProductStockUnavailable", $product->label), null, 'errors');
+								dol_syslog($langs->trans("ErrorProductStockUnavailable", $product->label), LOG_ERR);
+								$error++;
+								break;
+							}
+
+							// Parcours des dépôts
+							foreach($product->stock_warehouse as $stock) {
+								//var_dump($stock);
+								// Recherche du batch/lot si présent dans le dépôt
+								//var_dump(array_keys($stock->detail_batch));
+								if(isset($stock->detail_batch[$batch])) {
+									$productbatch = $stock->detail_batch[$batch];
+									//var_dump($productbatch);
+									// Au mieux la qté restant à expédier, au pire ce qui reste dans le batch
+									$batchqty = min($productbatch->qty, $line->qty-$subtotalqty, $batch_detail['qte'], $batch_detail['qty']);
+									$batch_detail['qte'] -= $batchqty;
+									$batch_detail['qty'] -= $batchqty;
+									$subtotalqty += $batchqty;
+									$sub_qty[] = [
+										'q' => $batchqty, // the qty we want to move for this stock record
+										'id_batch' => $productbatch->id, // the id into llx_product_batch of stock record to move
+									];
+								}
+								// Assez => on stoppe
+								if ($subtotalqty >= $line->qty)
+									break;
+							}
+							// Assez => on stoppe
+							if ($subtotalqty >= $line->qty)
+								break;
+						}
+					}
+					else {
+						// Parcours des lot par ordre de DDM
+						foreach($productlots as $batch=>$batch_ddm) {
+							// Parcours des dépôts
+							foreach($product->stock_warehouse as $stock) {
+								//var_dump($stock);
+								// Recherche du batch/lot si présent dans le dépôt
+								//var_dump(array_keys($stock->detail_batch));
+								if(isset($stock->detail_batch[$batch])) {
+									$productbatch = $stock->detail_batch[$batch];
+									//var_dump($productbatch);
+									// Au mieux la qté restant à expédier, au pire ce qui reste dans le batch
+									$batchqty = min($productbatch->qty, $line->qty-$subtotalqty);
+									$subtotalqty += $batchqty;
+									$sub_qty[] = [
+										'q' => $batchqty, // the qty we want to move for this stock record
+										'id_batch' => $productbatch->id, // the id into llx_product_batch of stock record to move
+									];
+								}
+								// Assez => on stoppe
+								if ($subtotalqty >= $line->qty)
+									break;
+							}
+							// Assez => on stoppe
+							if ($subtotalqty >= $line->qty)
+								break;
+						}
+					}
+				}
+
+				$batch_line[$i]['detail'] = $sub_qty; // array of details
+				$batch_line[$i]['qty'] = $subtotalqty;
+				$batch_line[$i]['ix_l'] = $line->id;
+				//var_dump($batch_line[$i]);
+			} else { // @todo finir propduits
+				foreach($product->stock_warehouse as $warehouse_id=>$stock) {
+					//var_dump($stock);
+	
+					$stockqty = min($stock->real, $line->qty-$subtotalqty);
+					
+					$stockLine[$i][] = [
+						'qty' => $stockqty,
+						'warehouse_id' => $warehouse_id,
+						'ix_l' => $line->id,
+					];
+					$subtotalqty += $stockqty;
+
+					if ($subtotalqty >= $line->qty)
+						break;
+				}
+			}
+			//var_dump($subtotalqty);
+
+			// Pas assez de produit -> on stoppe
+			if ($subtotalqty < $line->qty) {
+				//var_dump($product->label);
+				setEventMessages($langs->trans("ErrorProductStockUnavailable", $product->label), null, 'errors');
+				dol_syslog($langs->trans("ErrorProductStockUnavailable", $product->label), LOG_ERR);
+				$error++;
+				break;
+			}
+
+			$totalqty += $subtotalqty;
+		}
+
+		//var_dump($batch_line[2]);
+
+		// Ajout lignes
+		if (!$error) {
+			if ($totalqty > 0) {		// There is at least one thing to ship
+				//var_dump($_POST);exit;
+				for ($i = 0; $i < $num; $i++) {
+					// Vu la construction c'est l'un ou l'autre
+					if (isset($batch_line[$i])) {
+						// batch mode
+						$ret = $object->addline_batch($batch_line[$i], $array_options[$i]);
+						if ($ret < 0) {
+							setEventMessages($object->error, $object->errors, 'errors');
+							$error++;
+						}
+					}
+					elseif (isset($stockLine[$i])) {
+						//shipment from multiple stock locations
+						$nbstockline = count($stockLine[$i]);
+						for ($j = 0; $j < $nbstockline; $j++) {
+							$ret = $object->addline($stockLine[$i][$j]['warehouse_id'], $stockLine[$i][$j]['ix_l'], $stockLine[$i][$j]['qty'], $array_options[$i]);
+							if ($ret < 0) {
+								setEventMessages($object->error, $object->errors, 'errors');
+								$error++;
+							}
+						}
+					}
+				}
+				// Fill array 'array_options' with data from add form
+				$ret = $extrafields->setOptionalsFromPost(null, $object);
+				if ($ret < 0) {
+					$error++;
+				}
+
+				if (!$error) {
+					$ret = $object->create($user); // This create shipment (like Odoo picking) and lines of shipments. Stock movement will be done when validating shipment.
+					if ($ret <= 0) {
+						setEventMessages($object->error, $object->errors, 'errors');
+						$error++;
+					}
+				}
+			} else {
+				setEventMessages($langs->trans("ErrorEmptyShipping"), null, 'errors');
+				$error++;
+			}
+		}
+
+		// Validation
+		if (!$error) {
+			if ($validate) {
+				$result = $object->valid($user);
+				if ($result<0) {
+					setEventMessages($object->error, $object->errors, 'errors');
+					dol_syslog(get_class().' ::' .$object->error.implode(',',$object->errors), LOG_ERR);
+					$error++;
+				}
+				// Auto close shipping
+				if (
+					!empty($conf->global->MMI_ORDER_1CLIC_INVOICE_SHIPPING_AUTOCLOSE)
+					|| (!empty($conf->global->MMIPAYMENTS_CAISSE_USER) && $conf->global->MMIPAYMENTS_CAISSE_USER==$user->id)
+					|| (!empty($conf->global->MMIPAYMENTS_CAISSE_COMPANY) && $conf->global->MMIPAYMENTS_CAISSE_COMPANY==$order->thirdparty->id)
+				) {
+					$result =  $object->setClosed();
+					if ($result<0) {
+						setEventMessages($object->error, $object->errors, 'errors');
+						dol_syslog(get_class().' ::' .$object->error.implode(',',$object->errors), LOG_ERR);
+						$error++;
+					}
+				}
+			}
+		}
+
+		// Génération PDF
+		if (!$error) {
+			// Retrieve everything
+			$object->fetch($object->id);
+
+			// @todo : Niet! il faut mettre espadon mais d'abord vérifier qu'il est bien à jour
+			$docmodel = 'rouget';
+			$object->generateDocument($docmodel, $langs);
+		}
+
+		// OK ou rollback
+		if (!$error) {
+			$db->commit();
+			
+			return $object;
+			//var_dump($expe);
+		} else {
+			$db->rollback();
+		}
+	}
+
+	public static function invoice_draftify($user, Facture $invoice)
+	{
+		global $db;
+
+		return $db->query("UPDATE ".MAIN_DB_PREFIX."facture i
+			LEFT JOIN ".MAIN_DB_PREFIX."accounting_bookkeeping j ON j.doc_type='customer_invoice' AND j.fk_doc=i.rowid
+			SET i.fk_statut=0
+			WHERE i.rowid=".$invoice->id." AND j.rowid IS NULL");
+	}
+
+	public static function order_1clic_invoice_shipping($user, Commande $order)
+	{
+		global $conf;
+
+		if ($user->rights->expedition->creer) {
+			if (! ($expe = static::order_1clic_shipping($user, $order, true))) {
+				return;
+			}
+		}
+
+		// Création Facture
+		// @todo check if invoice not already done !
+		if (!empty($conf->global->MMI_ORDER_1CLIC_INVOICE) && empty($conf->global->MMI_ORDER_1CLIC_INVOICE_DELAY) && $user->rights->facture->creer) {
+			if (! ($invoice = static::order_1clic_invoice($user, $order, true))) {
+				return;
+			}
+		}
+
+		return true;
+	}
+
+
+	/**
+	 * Calcul des expéditions commande client
+	 */
+	public static function commande_expedition($id)
+	{
+		global $db;
+
+		$sql = 'SELECT cl.rowid, cl.qty, SUM(ed.qty) qty_expe
+			FROM '.MAIN_DB_PREFIX.'commandedet cl
+			LEFT JOIN '.MAIN_DB_PREFIX.'expeditiondet ed ON ed.fk_origin_line=cl.rowid
+			LEFT JOIN '.MAIN_DB_PREFIX.'product p ON p.rowid=cl.fk_product
+			LEFT JOIN '.MAIN_DB_PREFIX.'product_extrafields p2 ON p2.fk_object=cl.fk_product
+			WHERE cl.fk_commande='.$id.' AND cl.qty > 0 AND cl.product_type=0 AND (p2.rowid IS NULL OR p2.compose IS NULL OR p2.compose=0)
+			GROUP BY cl.rowid
+			HAVING qty_expe IS NULL OR cl.qty != qty_expe';
+		//var_dump($sql); //die();
+		$q = $db->query($sql);
+		$expe_ok = ($q->num_rows == 0 ?1 :0);
+		//var_dump($expe_ok); //die();
+		$sql = 'SELECT rowid, expe_ok
+			FROM '.MAIN_DB_PREFIX.'commande_extrafields
+			WHERE fk_object='.$id;
+		$q = $db->query($sql);
+		if (list($rowid, $expe_ok_old)=$q->fetch_row()) {
+			if ($expe_ok_old == $expe_ok)
+				return;
+			$sql = 'UPDATE '.MAIN_DB_PREFIX.'commande_extrafields
+				SET expe_ok='.$expe_ok.'
+				WHERE rowid='.$rowid;
+		}
+		else
+			$sql = 'INSERT INTO '.MAIN_DB_PREFIX.'commande_extrafields
+				(fk_object, expe_ok)
+				VALUES
+				('.$id.', '.$expe_ok.')';
+		//var_dump($sql);
+		$q = $db->query($sql);
+	}
+	
+	/**
+	 * Calcul des réceptions commande fournisseur
+	 */
+	public static function commande_four_reception($id)
+	{
+		global $user, $db;
+
+		$sql = 'SELECT cl.rowid, cl.qty, SUM(cd.qty) qty_recpt
+			FROM '.MAIN_DB_PREFIX.'commande_fournisseurdet cl
+			LEFT JOIN '.MAIN_DB_PREFIX.'commande_fournisseur_dispatch cd ON cd.fk_commande=cl.fk_commande AND cd.fk_commandefourndet=cl.rowid
+			LEFT JOIN '.MAIN_DB_PREFIX.'reception r ON r.rowid=cd.fk_reception
+			WHERE cl.fk_commande='.$id.'
+			GROUP BY cl.rowid
+			HAVING qty_recpt IS NULL OR cl.qty != qty_recpt';
+		//var_dump($sql); //die();
+		$q = $db->query($sql);
+		$recpt_ok = ($q->num_rows == 0 ?1 :0);
+		//var_dump($recpt_ok); //die();
+		$sql = 'SELECT rowid, recpt_ok
+			FROM '.MAIN_DB_PREFIX.'commande_fournisseur_extrafields
+			WHERE fk_object='.$id;
+		//var_dump($sql); //die();
+		$q = $db->query($sql);
+		//var_dump($q);
+		$update = false;
+		if (list($rowid, $recpt_ok_old)=$q->fetch_row()) {
+			//var_dump($rowid, $recpt_ok_old, $recpt_ok);
+			if ($recpt_ok_old != $recpt_ok) {
+				$sql = 'UPDATE '.MAIN_DB_PREFIX.'commande_fournisseur_extrafields
+					SET recpt_ok='.$recpt_ok.'
+					WHERE rowid='.$rowid;
+				$update = true;
+			}
+		}
+		else {
+			$sql = 'INSERT INTO '.MAIN_DB_PREFIX.'commande_fournisseur_extrafields
+				(fk_object, recpt_ok)
+				VALUES
+				('.$id.', '.$recpt_ok.')';
+			$update = true;
+		}
+		//var_dump($sql);
+		if ($update)
+			$q = $db->query($sql);
+		if ($recpt_ok) {
+			require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.commande.class.php';
+			$cmd = new CommandeFournisseur($db);
+			$cmd->fetch($id);
+			if ($cmd->statut==CommandeFournisseur::STATUS_RECEIVED_PARTIALLY) {
+				$cmd->statut = CommandeFournisseur::STATUS_RECEIVED_COMPLETELY;
+				$cmd->update($user);
+			}
+		}
+	}
+}
+
+mmi_workflow::__init();

+ 29 - 8
core/modules/modMMIWorkflow.class.php

@@ -118,6 +118,7 @@ class modMMIWorkflow extends DolibarrModules
 				'invoicecard',
 				'ordercard',
 				'pdfgeneration',
+				'shipmentlist'
 				//   'data' => array(
 				//       'hookcontext1',
 				//       'hookcontext2',
@@ -139,7 +140,7 @@ class modMMIWorkflow extends DolibarrModules
 		// A condition to hide module
 		$this->hidden = false;
 		// List of module class names as string that must be enabled if this module is enabled. Example: array('always1'=>'modModuleToEnable1','always2'=>'modModuleToEnable2', 'FR1'=>'modModuleToEnableFR'...)
-		$this->depends = array('modMMICommon');
+		$this->depends = array('modMMICommon', 'modMMIPayments');
 		$this->requiredby = array(); // List of module class names as string to disable if this one is disabled. Example: array('modModuleToDisable1', ...)
 		$this->conflictwith = array(); // List of module class names as string this module is in conflict with. Example: array('modModuleToDisable1', ...)
 
@@ -266,6 +267,16 @@ class modMMIWorkflow extends DolibarrModules
 		$this->rights = array();
 		$r = 0;
 		// Add here entries to declare new permissions
+		$this->rights[$r][0] = $this->numero . sprintf("%02d", $r + 1); // Permission id (must not be already used)
+		$this->rights[$r][1] = 'Réouvrir les commandes expédiées'; // Permission label
+		$this->rights[$r][4] = 'commande';
+		$this->rights[$r][5] = 'draftify';
+		$r++;
+		$this->rights[$r][0] = $this->numero . sprintf("%02d", $r + 1); // Permission id (must not be already used)
+		$this->rights[$r][1] = 'Réouvrir les factures expédiées'; // Permission label
+		$this->rights[$r][4] = 'facture';
+		$this->rights[$r][5] = 'draftify';
+		$r++;
 		/* BEGIN MODULEBUILDER PERMISSIONS */
 		/*
 		$this->rights[$r][0] = $this->numero . sprintf("%02d", $r + 1); // Permission id (must not be already used)
@@ -424,13 +435,23 @@ class modMMIWorkflow extends DolibarrModules
 		}
 
 		// Create extrafields during init
-		//include_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
-		//$extrafields = new ExtraFields($this->db);
-		//$result1=$extrafields->addExtraField('mmiworkflow_myattr1', "New Attr 1 label", 'boolean', 1,  3, 'thirdparty',   0, 0, '', '', 1, '', 0, 0, '', '', 'mmiworkflow@mmiworkflow', '$conf->mmiworkflow->enabled');
-		//$result2=$extrafields->addExtraField('mmiworkflow_myattr2', "New Attr 2 label", 'varchar', 1, 10, 'project',      0, 0, '', '', 1, '', 0, 0, '', '', 'mmiworkflow@mmiworkflow', '$conf->mmiworkflow->enabled');
-		//$result3=$extrafields->addExtraField('mmiworkflow_myattr3', "New Attr 3 label", 'varchar', 1, 10, 'bank_account', 0, 0, '', '', 1, '', 0, 0, '', '', 'mmiworkflow@mmiworkflow', '$conf->mmiworkflow->enabled');
-		//$result4=$extrafields->addExtraField('mmiworkflow_myattr4', "New Attr 4 label", 'select',  1,  3, 'thirdparty',   0, 1, '', array('options'=>array('code1'=>'Val1','code2'=>'Val2','code3'=>'Val3')), 1,'', 0, 0, '', '', 'mmiworkflow@mmiworkflow', '$conf->mmiworkflow->enabled');
-		//$result5=$extrafields->addExtraField('mmiworkflow_myattr5', "New Attr 5 label", 'text',    1, 10, 'user',         0, 0, '', '', 1, '', 0, 0, '', '', 'mmiworkflow@mmiworkflow', '$conf->mmiworkflow->enabled');
+		include_once DOL_DOCUMENT_ROOT.'/core/class/extrafields.class.php';
+		$extrafields = new ExtraFields($this->db);
+
+		// @ déplacer mmiworkflow + triggers & co
+
+		// Commande
+        $extrafields->addExtraField('expe_ok', $langs->trans('Extrafield_expe_ok'), 'boolean', 100, '', 'commande', 0, 0, '', "", 1, '', 5, $langs->trans('ExtrafieldToolTip_expe_ok'), '', $conf->entity, 'mmiworkflow@mmiworkflow', '$conf->mmiworkflow->enabled');
+
+		// Commande Fournisseur
+        $extrafields->addExtraField('recpt_ok', $langs->trans('Extrafield_recpt_ok'), 'boolean', 100, '', 'commande_fournisseur', 0, 0, '', "", 1, '', 5, $langs->trans('ExtrafieldToolTip_recpt_ok'), '', $conf->entity, 'mmiworkflow@mmiworkflow', '$conf->mmiworkflow->enabled');
+		
+		// thirdparty
+		$result1=$extrafields->addExtraField('pro', "Extrafield_client_pro", 'boolean', 1,  1, 'thirdparty',   0, 0, '0', '', 1, '', 1, 0, '', '', 'mmiworkflow@mmiworkflow', '$conf->mmiworkflow->enabled && $conf->global->SFYCUSTOM_FIELD_CLIENT_PRO');
+		$result1=$extrafields->addExtraField('invoice_noautosend', "Extrafield_invoice_noautosend", 'boolean', 1,  1, 'thirdparty',   0, 0, '0', '', 1, '', 1, 0, '', '', 'mmiworkflow@mmiworkflow', '$conf->mmiworkflow->enabled');
+
+		// facture
+		$result1=$extrafields->addExtraField('emailsent', "Extrafield_emailsent", 'boolean', 1,  1, 'facture',   0, 0, '1', '', 1, '', 1, 0, '', '', 'mmiworkflow@mmiworkflow', '$conf->mmiworkflow->enabled');
 
 		// Permissions
 		$this->remove($options);

+ 79 - 0
core/triggers/interface_99_modMMIWorkflow_MMIWorkflowTriggers.class.php

@@ -0,0 +1,79 @@
+<?php
+/* Copyright (C) 2022 Mathieu Moulin iProspective <contact@iprospective.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/>.
+ */
+
+dol_include_once('custom/mmicommon/class/MMITriggers.class.php');
+dol_include_once('/custom/mmiworkflow/class/mmi_workflow.class.php');
+
+/**
+ *  Class of triggers for SfyCustom module
+ */
+class InterfaceMMIWorkflowTriggers extends MMITriggers
+{
+	const MOD_NAME = 'mmiworkflow';
+	const DESC = 'MMIWorkflow triggers';
+
+	/**
+	 * Function called when a Dolibarrr business event is done.
+	 * All functions "runTrigger" are triggered if file
+	 * is inside directory core/triggers
+	 *
+	 * @param string 		$action 	Event action code
+	 * @param CommonObject 	$object 	Object
+	 * @param User 			$user 		Object user
+	 * @param Translate 	$langs 		Object langs
+	 * @param Conf 			$conf 		Object conf
+	 * @return int              		<0 if KO, 0 if no triggered ran, >0 if OK
+	 */
+	public function runTrigger($action, $object, User $user, Translate $langs, Conf $conf)
+	{
+		if (! $this->enabled()) {
+			return 0; // If module is not enabled, we do nothing
+		}
+
+		// Put here code you want to execute when a Dolibarr business events occurs.
+		// Data and type of action are stored into $object and $action
+
+		// You can isolate code for each action in a separate method: this method should be named like the trigger in camelCase.
+		// For example : COMPANY_CREATE => public function companyCreate($action, $object, User $user, Translate $langs, Conf $conf)
+		$methodName = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($action)))));
+		$callback = array($this, $methodName);
+		if (is_callable($callback)) {
+			dol_syslog(
+				"Trigger '".$this->name."' for action '$action' launched by ".__FILE__.". id=".$object->id
+			);
+
+			return call_user_func($callback, $action, $object, $user, $langs, $conf);
+		};
+
+		// Or you can execute some code here
+		switch ($action) {
+
+			case 'ORDER_CLOSE':
+				if ($conf->global->MMI_ORDER_1CLIC_INVOICE && $conf->global->MMI_ORDER_1CLIC_INVOICE_DELAY) {
+					mmi_workflow::order_1clic_invoice($user, $object);
+				}
+				break;
+
+			default:
+				dol_syslog("Trigger '".$this->name."' for action '$action' launched by ".__FILE__.". id=".$object->id);
+				break;
+			
+		}
+
+		return 0;
+	}
+}

+ 351 - 0
core/triggers/interface_99_modMMIWorkflow_Workflow.class.php

@@ -0,0 +1,351 @@
+<?php
+/* Copyright (C) 2021-2022		Mathieu Moulin		<contact@iprospective.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/>.
+ */
+
+/**
+ * \file    htdocs/core/triggers/interface_99_all_ERPSync.class.php
+ * \ingroup MMIPrestaSync
+ * \brief   Trigger for Synchronisation with Prestashop.
+ */
+
+require_once DOL_DOCUMENT_ROOT.'/core/triggers/dolibarrtriggers.class.php';
+dol_include_once('custom/mmiworkflow/class/mmi_workflow.class.php');
+
+/**
+ *  Class of triggers for MyModule module
+ */
+class InterfaceWorkflow extends DolibarrTriggers
+{
+	public static function __init()
+	{
+	}
+
+	/**
+	 * Constructor
+	 *
+	 * @param DoliDB $db Database handler
+	 */
+	public function __construct($db)
+	{
+		//die('MODULE BUILDER OK');
+		$this->db = $db;
+
+		$this->name = preg_replace('/^Interface/i', '', get_class($this));
+		$this->family = "demo";
+		$this->description = "MMIWorkflow triggers.";
+		// 'development', 'experimental', 'dolibarr' or version
+		$this->version = 'development';
+		$this->picto = 'logo@mmiworkflow';
+	}
+
+	/**
+	 * Trigger name
+	 *
+	 * @return string Name of trigger file
+	 */
+	public function getName()
+	{
+		return $this->name;
+	}
+
+	/**
+	 * Trigger description
+	 *
+	 * @return string Description of trigger file
+	 */
+	public function getDesc()
+	{
+		return $this->description;
+	}
+
+	/**
+	 * Function called when a Dolibarrr business event is done.
+	 * All functions "runTrigger" are triggered if file
+	 * is inside directory core/triggers
+	 *
+	 * @param string 		$action 	Event action code
+	 * @param CommonObject 	$object 	Object
+	 * @param User 			$user 		Object user
+	 * @param Translate 	$langs 		Object langs
+	 * @param Conf 			$conf 		Object conf
+	 * @return int              		<0 if KO, 0 if no triggered ran, >0 if OK
+	 */
+	public function runTrigger($action, $object, User $user, Translate $langs, Conf $conf)
+	{
+		//var_dump($action); var_dump($object); die();
+		// You can isolate code for each action in a separate method: this method should be named like the trigger in camelCase.
+		// For example : COMPANY_CREATE => public function companyCreate($action, $object, User $user, Translate $langs, Conf $conf)
+		$methodName = lcfirst(str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($action)))));
+		//var_dump($action); echo '<p>'.$methodName.'</p>'; //die();
+		//die();
+		$callback = array($this, $methodName);
+		if (is_callable($callback)) {
+			dol_syslog(
+				"Trigger '".$this->name."' for action '$action' launched by ".__FILE__.". id=".$object->id
+			);
+
+			return call_user_func($callback, $action, $object, $user, $langs, $conf);
+		};
+		
+		//var_dump('<p>MMI_DEBUG 	ACTION: '.$action); die();
+
+		// Or you can execute some code here
+		switch ($action) {
+			// Users
+			//case 'USER_CREATE':
+			//case 'USER_MODIFY':
+			//case 'USER_NEW_PASSWORD':
+			//case 'USER_ENABLEDISABLE':
+			//case 'USER_DELETE':
+
+			// Actions
+			//case 'ACTION_MODIFY':
+			//case 'ACTION_CREATE':
+			//case 'ACTION_DELETE':
+
+			// Groups
+			//case 'USERGROUP_CREATE':
+			//case 'USERGROUP_MODIFY':
+			//case 'USERGROUP_DELETE':
+
+			// Companies
+			//case 'COMPANY_CREATE':
+			//case 'COMPANY_MODIFY':
+			//case 'COMPANY_DELETE':
+
+			// Contacts
+			//case 'CONTACT_CREATE':
+			//case 'CONTACT_MODIFY':
+			//case 'CONTACT_DELETE':
+			//case 'CONTACT_ENABLEDISABLE':
+
+			// Products
+			//case 'PRODUCT_CREATE':
+			//case 'PRODUCT_MODIFY':
+			//case 'PRODUCT_PRICE_MODIFY':
+			//case 'PRODUCT_SOUSPRODUIT':
+			//case 'PRODUCT_DELETE':
+			//case 'PRODUCT_SET_MULTILANGS':
+			//case 'PRODUCT_DEL_MULTILANGS':
+
+			//case 'PRODUCTLOT_CREATE':
+			//case 'PRODUCTLOT_MODIFY':
+
+			//Stock mouvement
+			//case 'STOCK_MOVEMENT':
+			
+			//case 'SUPPLIER_PRODUCT_BUYPRICE_UPDATE':
+			//case 'SUPPLIER_PRODUCT_BUYPRICE_MODIFY':
+			
+			//MYECMDIR
+			//case 'MYECMDIR_CREATE':
+			//case 'MYECMDIR_MODIFY':
+			//case 'MYECMDIR_DELETE':
+
+			// Customer orders
+			//case 'ORDER_CREATE':
+			//case 'ORDER_MODIFY':
+			//case 'ORDER_VALIDATE':
+			//case 'ORDER_CANCEL':
+			//case 'ORDER_SENTBYMAIL':
+			//case 'ORDER_CLASSIFY_BILLED':
+			//case 'ORDER_SETDRAFT':
+			//case 'ORDER_DELETE':
+
+			//case 'LINEORDER_INSERT':
+			//case 'LINEORDER_UPDATE':
+			//case 'LINEORDER_DELETE':
+			
+			// Réceptions
+			//case 'RECEPTION_CREATE':
+			case 'RECEPTION_VALIDATE':
+			case 'RECEPTION_DELETE':
+			case 'RECEPTION_MODIFY':
+				//var_dump($object); //die();
+				if (!in_array($object->origin, ['commandeFournisseur', 'order_supplier']) || empty($object->origin_id))
+					break;
+				mmi_workflow::commande_four_reception($object->origin_id);
+				break;
+				
+			// Supplier orders
+			//case 'ORDER_SUPPLIER_CREATE':
+			case 'ORDER_SUPPLIER_MODIFY':
+			case 'ORDER_SUPPLIER_VALIDATE':
+			case 'ORDER_SUPPLIER_APPROVE':
+			//case 'ORDER_SUPPLIER_REFUSE':
+			//case 'ORDER_SUPPLIER_CANCEL':
+			//case 'ORDER_SUPPLIER_SENTBYMAIL':
+			case 'ORDER_SUPPLIER_DISPATCH':
+				//var_dump($object); die();
+				mmi_workflow::commande_four_reception($object->id);
+				break;
+			//case 'ORDER_SUPPLIER_DELETE':
+			//case 'LINEORDER_SUPPLIER_DISPATCH':
+			//case 'LINEORDER_SUPPLIER_CREATE':
+			//case 'LINEORDER_SUPPLIER_UPDATE':
+			//case 'LINEORDER_SUPPLIER_DELETE':
+
+			// Proposals
+			//case 'PROPAL_CREATE':
+			//case 'PROPAL_MODIFY':
+			//case 'PROPAL_VALIDATE':
+			//case 'PROPAL_SENTBYMAIL':
+			//case 'PROPAL_CLOSE_SIGNED':
+			//case 'PROPAL_CLOSE_REFUSED':
+			//case 'PROPAL_DELETE':
+			//case 'LINEPROPAL_INSERT':
+			//case 'LINEPROPAL_UPDATE':
+			//case 'LINEPROPAL_DELETE':
+
+			// SupplierProposal
+			//case 'SUPPLIER_PROPOSAL_CREATE':
+			//case 'SUPPLIER_PROPOSAL_MODIFY':
+			//case 'SUPPLIER_PROPOSAL_VALIDATE':
+			//case 'SUPPLIER_PROPOSAL_SENTBYMAIL':
+			//case 'SUPPLIER_PROPOSAL_CLOSE_SIGNED':
+			//case 'SUPPLIER_PROPOSAL_CLOSE_REFUSED':
+			//case 'SUPPLIER_PROPOSAL_DELETE':
+			//case 'LINESUPPLIER_PROPOSAL_INSERT':
+			//case 'LINESUPPLIER_PROPOSAL_UPDATE':
+			//case 'LINESUPPLIER_PROPOSAL_DELETE':
+
+			// Contracts
+			//case 'CONTRACT_CREATE':
+			//case 'CONTRACT_MODIFY':
+			//case 'CONTRACT_ACTIVATE':
+			//case 'CONTRACT_CANCEL':
+			//case 'CONTRACT_CLOSE':
+			//case 'CONTRACT_DELETE':
+			//case 'LINECONTRACT_INSERT':
+			//case 'LINECONTRACT_UPDATE':
+			//case 'LINECONTRACT_DELETE':
+
+			// Bills
+			//case 'BILL_CREATE':
+			//case 'BILL_MODIFY':
+			//case 'BILL_VALIDATE':
+			//case 'BILL_UNVALIDATE':
+			//case 'BILL_SENTBYMAIL':
+			//case 'BILL_CANCEL':
+			//case 'BILL_PAYED':
+			//case 'BILL_DELETE':
+			
+			//case 'LINEBILL_INSERT':
+			//case 'LINEBILL_UPDATE':
+			//case 'LINEBILL_DELETE':
+
+			//Supplier Bill
+			//case 'BILL_SUPPLIER_CREATE':
+			//case 'BILL_SUPPLIER_UPDATE':
+			//case 'BILL_SUPPLIER_DELETE':
+			//case 'BILL_SUPPLIER_PAYED':
+			//case 'BILL_SUPPLIER_UNPAYED':
+			//case 'BILL_SUPPLIER_VALIDATE':
+			//case 'BILL_SUPPLIER_UNVALIDATE':
+			//case 'LINEBILL_SUPPLIER_CREATE':
+			//case 'LINEBILL_SUPPLIER_UPDATE':
+			//case 'LINEBILL_SUPPLIER_DELETE':
+
+			// Payments
+			//case 'PAYMENT_CUSTOMER_CREATE':
+			//case 'PAYMENT_SUPPLIER_CREATE':
+			//case 'PAYMENT_ADD_TO_BANK':
+			//case 'PAYMENT_DELETE':
+
+			// Online
+			//case 'PAYMENT_PAYBOX_OK':
+			//case 'PAYMENT_PAYPAL_OK':
+			//case 'PAYMENT_STRIPE_OK':
+
+			// Donation
+			//case 'DON_CREATE':
+			//case 'DON_UPDATE':
+			//case 'DON_DELETE':
+
+			// Interventions
+			//case 'FICHINTER_CREATE':
+			//case 'FICHINTER_MODIFY':
+			//case 'FICHINTER_VALIDATE':
+			//case 'FICHINTER_DELETE':
+			//case 'LINEFICHINTER_CREATE':
+			//case 'LINEFICHINTER_UPDATE':
+			//case 'LINEFICHINTER_DELETE':
+
+			// Members
+			//case 'MEMBER_CREATE':
+			//case 'MEMBER_VALIDATE':
+			//case 'MEMBER_SUBSCRIPTION':
+			//case 'MEMBER_MODIFY':
+			//case 'MEMBER_NEW_PASSWORD':
+			//case 'MEMBER_RESILIATE':
+			//case 'MEMBER_DELETE':
+
+			// Categories
+			//case 'CATEGORY_CREATE':
+			//case 'CATEGORY_MODIFY':
+			//case 'CATEGORY_DELETE':
+			//case 'CATEGORY_SET_MULTILANGS':
+
+			// Projects
+			//case 'PROJECT_CREATE':
+			//case 'PROJECT_MODIFY':
+			//case 'PROJECT_DELETE':
+
+			// Project tasks
+			//case 'TASK_CREATE':
+			//case 'TASK_MODIFY':
+			//case 'TASK_DELETE':
+
+			// Task time spent
+			//case 'TASK_TIMESPENT_CREATE':
+			//case 'TASK_TIMESPENT_MODIFY':
+			//case 'TASK_TIMESPENT_DELETE':
+			//case 'PROJECT_ADD_CONTACT':
+			//case 'PROJECT_DELETE_CONTACT':
+			//case 'PROJECT_DELETE_RESOURCE':
+
+			// Shipping
+			//case 'SHIPPING_CREATE':
+			case 'SHIPPING_MODIFY':
+			case 'SHIPPING_VALIDATE':
+			//case 'SHIPPING_SENTBYMAIL':
+			case 'SHIPPING_BILLED':
+			case 'SHIPPING_CLOSED':
+			case 'SHIPPING_REOPEN':
+				if (!in_array($object->origin, ['commande', 'order']) || empty($object->origin_id))
+					break;
+				
+				mmi_workflow::commande_expedition($object->origin_id);
+				break;
+			case 'SHIPPING_DELETE':
+				if (!in_array($object->origin, ['commande', 'order']) || empty($object->origin_id))
+					break;
+				
+				mmi_workflow::commande_expedition($object->origin_id);
+				break;
+
+			// and more...
+
+			default:
+				dol_syslog("Trigger '".$this->name."' for action '$action' launched by ".__FILE__.". id=".$object->id);
+				break;
+		}
+
+		return 0;
+	}
+}
+
+InterfaceWorkflow::__init();

+ 97 - 0
langs/fr_FR/mmiworkflow.lang

@@ -0,0 +1,97 @@
+# Copyright (C) 2022 Mathieu Moulin <mathieu@iprospective.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/>.
+
+#
+# Generic
+#
+
+# Module label 'ModuleMMIWorkflowName'
+ModuleMMIWorkflowName = Amélioration Workflow
+# Module description 'ModuleMMIWorkflowDesc'
+ModuleMMIWorkflowDesc = Ensemble de paramètres et aléliorations du workflow  Devis/Commande/Facture/Expé, expédition/facturation en 1 clic, facturation à la livraison, envoi automatique de facture par email, etc.
+
+#
+# Admin page
+#
+MMIWorkflowSetup = MMIWorkflow setup
+Settings = Settings
+MMIWorkflowSetupPage = MMIWorkflow setup page
+
+SFYCUSTOM_FIELD_CLIENT_PRO = Champ "Professionnel" sur fiche client
+MMI_ORDER_1CLIC_INVOICE_SHIPPING = Expédition (et facture) en 1 clic
+MMI_ORDER_1CLIC_INVOICE_SHIPPING_AUTOCLOSE = Clôturer l'expé automatiquement
+MMI_ORDER_1CLIC_INVOICE = Facturer automatiquement (par défaut à l'expédition)
+MMI_ORDER_1CLIC_INVOICE_DELAY = Décaler la facturation à la livraison
+MMI_ORDER_1CLIC_INVOICE_EMAIL_AUTO = Envoyer automatiquement la facture par email
+MMI_ORDER_1CLIC_INVOICE_EMAIL_AUTO_NOPRO = Ne pas envoyer la facture automatiquement pour les pro
+MMI_SHIPPING_PDF_MESSAGE = Message à afficher en en-tête des expéditions
+MAIN_GENERATE_SHIPMENT_WITH_PICTURE = Afficher les images sur les PDF des expéditions
+SHIPPING_PDF_HIDE_BATCH = Cacher le numéro de lot dans les expéditions
+SHIPPING_PDF_HIDE_WEIGHT_AND_VOLUME = Cacher la colonne poids/volume dans les expéditions
+SHIPPING_PDF_HIDE_DELIVERY_DATE = Cacher la date de livraison dans les expéditions
+SHIPPING_PDF_ANTIGASPI = Afficher anti-gaspi sur les expéditions
+MMI_INVOICE_DRAFTIFY = Permettre aux admin de remettre les factures payées en brouillon
+MMI_INVOICE_DRAFTIFY_TYPES = Types de factures que l'on peut remettre en brouillon
+MMI_ORDER_DRAFTIFY = Permettre aux admin de remettre les commandes expédiées en brouillon
+MMI_INVOICE_EMAILSEND = Bouton pour envoyer des factures par email directement
+MMI_1CT_FIX = Bouton pour corriger les erreurs de "1ct"
+MMI_1CT_DIFFLIMIT = Seuil de tolérance (3ct par défaut) pour recalculer les lignes de commande/facture et faire matcher avec le réglement (0.03 conseillé, correspondant à la totalité des bugs liés à Prestashop)
+MMI_VAT_TX_FIX = Bouton pour corriger les erreurs de synchro de TVA
+SFY_ALERT_ORDER_NOT_SHIPPED = Activer une alerte sur la liste des expeditions si il existe des commandes validées
+MMI_MOVEMENT_LIST_ENHANCE = AJouter des informations (client, commande...) dans le liste des mouvements de stock
+SFYCUSTOM_LOCK = Bloquer la validation des commandes si lignes libres
+
+#
+# Extrafields
+#
+Extrafield_recpt_ok = Reçu tout
+Extrafield_expe_ok = Expédié tout
+Extrafield_client_pro = Professionnel
+Extrafield_emailsent = Envoyé par email
+Extrafield_invoice_noautosend = Ne pas envoyer automatiquement les factures
+
+#
+# About page
+#
+About = About
+MMIWorkflowAbout = About MMIWorkflow
+MMIWorkflowAboutPage = MMIWorkflow about page
+
+#
+# Sample page
+#
+MyPageName = My page name
+
+#
+# Sample widget
+#
+MyWidget = My widget
+MyWidgetDescription = My widget description
+
+#
+# Boutons
+#
+MMIInvoiceDraftify = Réouvrir en Brouillon
+MMI1ClickOrderInvoiceShipping = 1 Clic Facture & Expé
+MMI1ClickOrderInvoiceShippingConfirm = Êtes-vous certain de vouloir FActurer et Expédier ?
+MMIInvoiceEmailSend = Envoi express email
+MMI1ctFix = Fix Bug 1ct
+
+#
+# Messages
+#
+OrderInValidatedStatusAlert=Certaines commandes n'ont pas d'expédition: %s
+ErrorProductStockUnavailable = Stock insuffisant pour le produit : %s
+SfCstCannotValidWithFreeLine = Ligne(s) libre(s) !! Non autorisé(s) pour validation