diff --git a/doc/doctemplates/invoices/template_invoice.odt b/doc/doctemplates/invoices/template_invoice.odt new file mode 100644 index 0000000000000..925d9f087cdfc Binary files /dev/null and b/doc/doctemplates/invoices/template_invoice.odt differ diff --git a/doc/doctemplates/orders/template_order.odt b/doc/doctemplates/orders/template_order.odt new file mode 100644 index 0000000000000..b9f5162316d85 Binary files /dev/null and b/doc/doctemplates/orders/template_order.odt differ diff --git a/doc/doctemplates/supplier_invoices/template_supplier_invoices.odt b/doc/doctemplates/supplier_invoices/template_supplier_invoices.odt new file mode 100644 index 0000000000000..8ece83c989c9f Binary files /dev/null and b/doc/doctemplates/supplier_invoices/template_supplier_invoices.odt differ diff --git a/doc/doctemplates/supplier_orders/template_supplier_order.odt b/doc/doctemplates/supplier_orders/template_supplier_order.odt new file mode 100644 index 0000000000000..e5020aef036eb Binary files /dev/null and b/doc/doctemplates/supplier_orders/template_supplier_order.odt differ diff --git a/doc/doctemplates/thirdparties/template_thirdparty.odt b/doc/doctemplates/thirdparties/template_thirdparty.odt new file mode 100644 index 0000000000000..bfc4fa822f28f Binary files /dev/null and b/doc/doctemplates/thirdparties/template_thirdparty.odt differ diff --git a/doc/medias/README.txt b/doc/medias/README.txt new file mode 100644 index 0000000000000..1495e709d92fd --- /dev/null +++ b/doc/medias/README.txt @@ -0,0 +1,2 @@ +# File(s) into root medias directory are provide by Pixabay with Licence "CC0 Creative Commons" +# https://creativecommons.org/publicdomain/zero/1.0/ diff --git a/doc/medias/background_coder.jpg b/doc/medias/background_coder.jpg new file mode 100644 index 0000000000000..326d453c8f528 Binary files /dev/null and b/doc/medias/background_coder.jpg differ diff --git a/doc/medias/background_dolibarr.jpg b/doc/medias/background_dolibarr.jpg new file mode 100644 index 0000000000000..6c4cc11460d33 Binary files /dev/null and b/doc/medias/background_dolibarr.jpg differ diff --git a/doc/medias/background_rough-horn.jpg b/doc/medias/background_rough-horn.jpg new file mode 100644 index 0000000000000..662ac02f25ffa Binary files /dev/null and b/doc/medias/background_rough-horn.jpg differ diff --git a/doc/medias/background_sunset.jpg b/doc/medias/background_sunset.jpg new file mode 100644 index 0000000000000..63c6100f9646e Binary files /dev/null and b/doc/medias/background_sunset.jpg differ diff --git a/htdocs/accountancy/admin/defaultaccounts.php b/htdocs/accountancy/admin/defaultaccounts.php index 3eace90837308..235bba4260552 100644 --- a/htdocs/accountancy/admin/defaultaccounts.php +++ b/htdocs/accountancy/admin/defaultaccounts.php @@ -98,6 +98,9 @@ if (getDolGlobalString('INVOICE_USE_RETAINED_WARRANTY')) { $list_account[] = 'ACCOUNTING_ACCOUNT_CUSTOMER_RETAINED_WARRANTY'; } +if (getDolGlobalString('INVOICE_USE_PRORATA_DISCOUNT')) { + $list_account[] = 'ACCOUNTING_ACCOUNT_CUSTOMER_PRORATA_DISCOUNT'; +} if (isModEnabled('don')) { $list_account[] = 'DONATION_ACCOUNTINGACCOUNT'; } diff --git a/htdocs/accountancy/class/lettering.class.php b/htdocs/accountancy/class/lettering.class.php index f8dd235b6c604..f7af7e1c85c55 100644 --- a/htdocs/accountancy/class/lettering.class.php +++ b/htdocs/accountancy/class/lettering.class.php @@ -489,7 +489,7 @@ public function bookkeepingLettering($bookkeeping_ids, $unlettering = false) } // Check balance amount - if (!$group_error && !$unlettering && price2num($total) != 0) { + if (!$group_error && !$unlettering && price2num($total) < 0.01) { $this->errors[] = $langs->trans('AccountancyErrorMismatchBalanceAmount', $total); $group_error++; } diff --git a/htdocs/accountancy/journal/sellsjournal.php b/htdocs/accountancy/journal/sellsjournal.php index 7876415715dbe..2ffde28f6c9d4 100644 --- a/htdocs/accountancy/journal/sellsjournal.php +++ b/htdocs/accountancy/journal/sellsjournal.php @@ -117,7 +117,7 @@ $date_end = dol_get_last_day($pastmonthyear, $pastmonth, false); } -$sql = "SELECT f.rowid, f.ref, f.type, f.situation_cycle_ref, f.datef as df, f.ref_client, f.date_lim_reglement as dlr, f.close_code, f.retained_warranty, f.revenuestamp,"; +$sql = "SELECT f.rowid, f.ref, f.type, f.situation_cycle_ref, f.datef as df, f.ref_client, f.date_lim_reglement as dlr, f.close_code, f.retained_warranty, f.revenuestamp, f.prorata_discount, f.total_ht as fact_total_ht,"; $sql .= " fd.rowid as fdid, fd.description, fd.product_type, fd.total_ht, fd.total_tva, fd.total_localtax1, fd.total_localtax2, fd.tva_tx, fd.total_ttc, fd.situation_percent, fd.vat_src_code, fd.info_bits,"; $sql .= " s.rowid as socid, s.nom as name, s.code_client, s.code_fournisseur,"; if (getDolGlobalString('MAIN_COMPANY_PERENTITY_SHARED')) { @@ -189,6 +189,7 @@ $tabtva = array(); $def_tva = array(); $tabwarranty = array(); + $tabprorata = array(); $tabrevenuestamp = array(); $tabttc = array(); $tablocaltax1 = array(); @@ -263,6 +264,9 @@ } } + // Manage prorata_rate for VAT + $prorata_rate = $obj->fact_total_ht <> 0 ? ($obj->prorata_discount / $obj->fact_total_ht) : 0; + $revenuestamp = (float) price2num($obj->revenuestamp, 'MT'); // Invoice lines @@ -310,11 +314,19 @@ $total_ttc -= $retained_warranty; } + // Move a part of the prorata discount/charge into the account for prorata charges + if (getDolGlobalString('INVOICE_USE_PRORATA_DISCOUNT') && $obj->prorata_discount > 0) { + $prorata_discount = $prorata_rate * $obj->total_ht * $situation_ratio; + $tabprorata[$obj->rowid][$compta_soc] += $prorata_discount; + $total_ttc -= $prorata_discount; // Remove ht value for prorata + $total_ttc -= ($obj->total_tva * $situation_ratio * $prorata_rate); // Remove VAT value corresponding to prorata + } + $tabttc[$obj->rowid][$compta_soc] += $total_ttc; $tabht[$obj->rowid][$compta_prod] += $obj->total_ht * $situation_ratio; $tva_npr = (($obj->info_bits & 1 == 1) ? 1 : 0); if (!$tva_npr) { // We ignore line if VAT is a NPR - $tabtva[$obj->rowid][$compta_tva] += $obj->total_tva * $situation_ratio; + $tabtva[$obj->rowid][$compta_tva] += $obj->total_tva * $situation_ratio * (1 - $prorata_rate); } $tablocaltax1[$obj->rowid][$compta_localtax1] += $obj->total_localtax1 * $situation_ratio; $tablocaltax2[$obj->rowid][$compta_localtax2] += $obj->total_localtax2 * $situation_ratio; @@ -369,6 +381,18 @@ $errorforinvoice = array(); +// Round prorata and TVA to cents +if (getDolGlobalString('ROUND_IN_ACCOUNTING')) { + $to_round = array(&$tabprorata, &$tabtva, &$tabht, &$tabttc); + foreach ($to_round as &$tab) { + foreach ($tab as $invoice => $accounts) { + foreach ($accounts as $label => $value) { + $tab[$invoice][$label] = round($value, 2); + } + } + } +} + /* // Old way, 1 query for each invoice // Loop on all invoices to detect lines without binded code (fk_code_ventilation <= 0) @@ -426,14 +450,16 @@ $companystatic = new Societe($db); $invoicestatic = new Facture($db); - $accountingaccountcustomer = new AccountingAccount($db); + $accountingaccountcustomer = new AccountingAccount($db); $accountingaccountcustomer->fetch(null, getDolGlobalString('ACCOUNTING_ACCOUNT_CUSTOMER'), true); $accountingaccountcustomerwarranty = new AccountingAccount($db); - $accountingaccountcustomerwarranty->fetch(null, getDolGlobalString('ACCOUNTING_ACCOUNT_CUSTOMER_RETAINED_WARRANTY'), true); + $accountingaccountcustomerprorata = new AccountingAccount($db); + $accountingaccountcustomerprorata->fetch(null, getDolGlobalString('ACCOUNTING_ACCOUNT_CUSTOMER_PRORATA_DISCOUNT'), true); + foreach ($tabfac as $key => $val) { // Loop on each invoice $errorforline = 0; @@ -586,6 +612,67 @@ } } + // Prorata + if (!$errorforline && getDolGlobalString('INVOICE_USE_PRORATA_DISCOUNT') && isset($tabprorata[$key])) { + if (is_array($tabprorata[$key])) { + foreach ($tabprorata[$key] as $k => $mt) { + $bookkeeping = new BookKeeping($db); + $bookkeeping->doc_date = $val["date"]; + $bookkeeping->date_lim_reglement = $val["datereg"]; + $bookkeeping->doc_ref = $val["ref"]; + $bookkeeping->date_creation = $now; + $bookkeeping->doc_type = 'customer_invoice'; + $bookkeeping->fk_doc = $key; + $bookkeeping->fk_docdet = 0; // Useless, can be several lines that are source of this record to add + $bookkeeping->thirdparty_code = $companystatic->code_client; + + if (getDolGlobalString('ACCOUNTING_ACCOUNT_CUSTOMER_USE_AUXILIARY_ON_DEPOSIT')) { + if ($k == getDolGlobalString('ACCOUNTING_ACCOUNT_CUSTOMER_DEPOSIT')) { + $bookkeeping->subledger_account = $tabcompany[$key]['code_compta']; + $bookkeeping->subledger_label = $tabcompany[$key]['name']; + } else { + $bookkeeping->subledger_account = ''; + $bookkeeping->subledger_label = ''; + } + } else { + $bookkeeping->subledger_account = ''; + $bookkeeping->subledger_label = ''; + } + + $bookkeeping->numero_compte = getDolGlobalString('ACCOUNTING_ACCOUNT_CUSTOMER_PRORATA_DISCOUNT'); + $bookkeeping->label_compte = $accountingaccountcustomerprorata->label; + + $bookkeeping->label_operation = dol_trunc($companystatic->name, 16) . ' - ' . $invoicestatic->ref . ' - ' . $langs->trans("ProtataDiscountAccountancy"); + $bookkeeping->montant = $mt; + $bookkeeping->sens = ($mt >= 0) ? 'D' : 'C'; + $bookkeeping->debit = ($mt >= 0) ? $mt : 0; + $bookkeeping->credit = ($mt < 0) ? -$mt : 0; + $bookkeeping->code_journal = $journal; + $bookkeeping->journal_label = $langs->transnoentities($journal_label); + $bookkeeping->fk_user_author = $user->id; + $bookkeeping->entity = $conf->entity; + + $totaldebit += $bookkeeping->debit; + $totalcredit += $bookkeeping->credit; + + $result = $bookkeeping->create($user); + if ($result < 0) { + if ($bookkeeping->error == 'BookkeepingRecordAlreadyExists') { // Already exists + $error++; + $errorforline++; + $errorforinvoice[$key] = 'alreadyjournalized'; + //setEventMessages('Transaction for ('.$bookkeeping->doc_type.', '.$bookkeeping->fk_doc.', '.$bookkeeping->fk_docdet.') were already recorded', null, 'warnings'); + } else { + $error++; + $errorforline++; + $errorforinvoice[$key] = 'other'; + setEventMessages($bookkeeping->error, $bookkeeping->errors, 'errors'); + } + } + } + } + } + // Product / Service if (!$errorforline) { foreach ($tabht[$key] as $k => $mt) { @@ -1143,7 +1230,7 @@ function writebookkeeping() { $i++; continue; } - if ($errorforinvoice[$key] == 'somelinesarenotbound') { + if (array_key_exists($key, $errorforinvoice) && $errorforinvoice[$key] == 'somelinesarenotbound') { print ''; print ""; print "".$date.""; @@ -1228,6 +1315,37 @@ function writebookkeeping() { $i++; } + // Prorata + if (getDolGlobalString('INVOICE_USE_PRORATA_DISCOUNT') && isset($tabprorata[$key]) && is_array($tabprorata[$key])) { + foreach ($tabprorata[$key] as $k => $mt) { + print ''; + print ""; + print "" . $date . ""; + print "" . $invoicestatic->getNomUrl(1) . ""; + // Account + print ""; + $accountoshow = length_accountg(getDolGlobalString('ACCOUNTING_ACCOUNT_CUSTOMER_PRORATA_DISCOUNT')); + if (($accountoshow == "") || $accountoshow == 'NotDefined') { + print '' . $langs->trans("MainAccountForProrataNotDefined") . ''; + } else { + print $accountoshow; + } + print ''; + // Subledger account + print ""; + if (getDolGlobalString('ACCOUNTING_ACCOUNT_CUSTOMER_USE_AUXILIARY_ON_DEPOSIT')) { + if ($k == getDolGlobalString('ACCOUNTING_ACCOUNT_CUSTOMER_DEPOSIT')) { + print length_accounta($tabcompany[$key]['code_compta']); + } + } + print ''; + print "" . $companystatic->getNomUrl(0, 'customer', 16) . ' - ' . $invoicestatic->ref . ' - ' . $langs->trans("ProtataDiscountAccountancy") . ""; + print '' . ($mt >= 0 ? price($mt) : '') . ""; + print '' . ($mt < 0 ? price(-$mt) : '') . ""; + print ""; + } + } + // Product / Service foreach ($tabht[$key] as $k => $mt) { $accountingaccount = new AccountingAccount($db); diff --git a/htdocs/comm/propal/card.php b/htdocs/comm/propal/card.php index 0d0a7f2e37a07..93dda1e6ba1c3 100644 --- a/htdocs/comm/propal/card.php +++ b/htdocs/comm/propal/card.php @@ -84,6 +84,7 @@ $contactid = GETPOST('contactid', 'int'); $projectid = GETPOST('projectid', 'int'); $rank = (GETPOST('rank', 'int') > 0) ? GETPOST('rank', 'int') : -1; +$same_project_filter = GETPOST('sameproject', 'string') == "on"; // PDF $hidedetails = (GETPOST('hidedetails', 'int') ? GETPOST('hidedetails', 'int') : (getDolGlobalString('MAIN_GENERATE_DOCUMENTS_HIDE_DETAILS') ? 1 : 0)); @@ -1270,7 +1271,7 @@ } $type = $prod->type; - $fk_unit = $prod->fk_unit; + $fk_unit = (getDolGlobalInt('MAIN_EDIT_LINE_SET_UNIT_ON_EXISTING_PRODUCT') ? GETPOST('units', 'alpha') : $prod->fk_unit) ; } else { $pu_ht = price2num($price_ht, 'MU'); $pu_ttc = price2num($price_ttc, 'MU'); @@ -2224,7 +2225,7 @@ $title = $langs->trans('ProductsAndServices'); print load_fiche_titre($title); - print '
'; + print '
'; print ''; $objectsrc->printOriginLinesList(); @@ -2973,7 +2974,7 @@ include DOL_DOCUMENT_ROOT.'/core/tpl/ajaxrow.tpl.php'; } - print '
'; + print '
'; if (!empty($object->lines) || ($object->statut == Propal::STATUS_DRAFT && $usercancreate && $action != 'selectlines' && $action != 'editline')) { print '
'; } @@ -3155,7 +3156,7 @@ print $formfile->showdocuments('propal', $objref, $filedir, $urlsource, $genallowed, $delallowed, $object->model_pdf, 1, 0, 0, 28, 0, '', 0, '', $soc->default_lang, '', $object); // Show links to link elements - $linktoelem = $form->showLinkToObjectBlock($object, null, array('propal')); + $linktoelem = $form->showLinkToObjectBlock($object, null, array('propal'), $same_project_filter); $compatibleImportElementsList = false; if ($user->hasRight('propal', 'creer') && $object->statut == Propal::STATUS_DRAFT) { diff --git a/htdocs/commande/card.php b/htdocs/commande/card.php index 70eb27b3fbe6e..ab55dc1c1f502 100644 --- a/htdocs/commande/card.php +++ b/htdocs/commande/card.php @@ -92,6 +92,7 @@ $origin = GETPOST('origin', 'alpha'); $originid = (GETPOST('originid', 'int') ? GETPOST('originid', 'int') : GETPOST('origin_id', 'int')); // For backward compatibility $rank = (GETPOST('rank', 'int') > 0) ? GETPOST('rank', 'int') : -1; +$same_project_filter = GETPOST('sameproject', 'string') == "on"; // PDF $hidedetails = (GETPOST('hidedetails', 'int') ? GETPOST('hidedetails', 'int') : (getDolGlobalString('MAIN_GENERATE_DOCUMENTS_HIDE_DETAILS') ? 1 : 0)); @@ -970,7 +971,7 @@ } $type = $prod->type; - $fk_unit = $prod->fk_unit; + $fk_unit = (getDolGlobalInt('MAIN_EDIT_LINE_SET_UNIT_ON_EXISTING_PRODUCT') ? GETPOST('units', 'alpha') : $prod->fk_unit) ; } else { $pu_ht = price2num($price_ht, 'MU'); $pu_ttc = price2num($price_ttc, 'MU'); @@ -2122,7 +2123,7 @@ $title = $langs->trans('ProductsAndServices'); print load_fiche_titre($title); - print '
'; + print '
'; print '
'; $objectsrc->printOriginLinesList('', $selectedLines); @@ -2870,7 +2871,7 @@ include DOL_DOCUMENT_ROOT.'/core/tpl/ajaxrow.tpl.php'; } - print '
'; + print '
'; print '
'; // Show object lines @@ -3084,7 +3085,7 @@ // Show links to link elements - $linktoelem = $form->showLinkToObjectBlock($object, null, array('order')); + $linktoelem = $form->showLinkToObjectBlock($object, null, array('order'), $same_project_filter); $compatibleImportElementsList = false; if ($usercancreate diff --git a/htdocs/compta/facture/card-rec.php b/htdocs/compta/facture/card-rec.php index 9dd5e491a8259..66f58faab5b13 100644 --- a/htdocs/compta/facture/card-rec.php +++ b/htdocs/compta/facture/card-rec.php @@ -1184,7 +1184,7 @@ /* * Invoice lines */ - print '
'; + print '
'; print '
'; // Show object lines @@ -1683,7 +1683,7 @@ include DOL_DOCUMENT_ROOT.'/core/tpl/ajaxrow.tpl.php'; } - print '
'; + print '
'; print '
'; // Show object lines if (!empty($object->lines)) { diff --git a/htdocs/compta/facture/card.php b/htdocs/compta/facture/card.php index b94991ee34a50..7aed3307829d7 100644 --- a/htdocs/compta/facture/card.php +++ b/htdocs/compta/facture/card.php @@ -100,6 +100,7 @@ $rank = (GETPOST('rank', 'int') > 0) ? GETPOST('rank', 'int') : -1; $projectid = (GETPOST('projectid', 'int') ? GETPOST('projectid', 'int') : 0); $selectedLines = GETPOST('toselect', 'array'); +$same_project_filter = GETPOST('sameproject', 'string') == "on"; // PDF $hidedetails = (GETPOST('hidedetails', 'int') ? GETPOST('hidedetails', 'int') : (getDolGlobalString('MAIN_GENERATE_DOCUMENTS_HIDE_DETAILS') ? 1 : 0)); @@ -167,6 +168,12 @@ $retainedWarrantyInvoiceAvailableType = explode('+', getDolGlobalString('INVOICE_USE_RETAINED_WARRANTY')); } +// Invoice available type for prorata discount +$prorataInvoiceAvailableType = array(); +if (getDolGlobalString('INVOICE_USE_PRORATA_DISCOUNT')) { + $prorataInvoiceAvailableType = explode('+', getDolGlobalString('INVOICE_USE_PRORATA_DISCOUNT')); +} + // Security check if ($user->socid) { $socid = $user->socid; @@ -432,6 +439,47 @@ if ($result < 0) { dol_print_error($db, $object->error); } + } elseif ($action == 'setproratarate' && $usercancreate) { + // With prorata, if a discount if given, it prevails on amount. + $object->fetch($id); + $new_rate = GETPOST('prorata_rate', 'float') ? GETPOST('prorata_rate', 'float') : 0; + $new_discount = GETPOST('prorata_discount', 'float') ? GETPOST('prorata_discount', 'float') : 0; + // If rate has changed, overwrite - rounding is used to deal with very subtle variations + if (round($object->prorata_rate, 5) <> round($new_rate, 5)) { + $result = $object->setProrataFromRate($new_rate); + } elseif (round($object->prorata_discount, 5) <> round($new_discount, 5)) { + // If discount has changed, overwrite + $object->prorata_discount = $new_discount; + $object->prorata_rate = null; // Reset to null for discount to overwrite in update + $object->update($user); + } + if ($result < 0) { + dol_print_error($db, $object->error); + } else { + // Define output language + if (!getDolGlobalString('MAIN_DISABLE_PDF_AUTOUPDATE')) { + $outputlangs = $langs; + $newlang = ''; + if (getDolGlobalInt('MAIN_MULTILANGS') && empty($newlang) && GETPOST('lang_id', 'aZ09')) { + $newlang = GETPOST('lang_id', 'aZ09'); + } + if (getDolGlobalInt('MAIN_MULTILANGS') && empty($newlang)) { + $newlang = $object->thirdparty->default_lang; + } + if (!empty($newlang)) { + $outputlangs = new Translate("", $conf); + $outputlangs->setDefaultLang($newlang); + $outputlangs->load('products'); + } + $model = $object->model_pdf; + $ret = $object->fetch($id); // Reload to get new records + + $result = $object->generateDocument($model, $outputlangs, $hidedetails, $hidedesc, $hideref); + if ($result < 0) { + setEventMessages($object->error, $object->errors, 'errors'); + } + } + } } elseif ($action == 'setmulticurrencycode' && $usercancreate) { // Multicurrency Code $result = $object->setMulticurrencyCode(GETPOST('multicurrency_code', 'alpha')); } elseif ($action == 'setmulticurrencyrate' && $usercancreate) { // Multicurrency rate @@ -1994,6 +2042,115 @@ $nextSituationInvoice = new Facture($db); $nextSituationInvoice->fetch($id); + // Get the id of the client order to include + $order_to_merge = GETPOST('commandeid', 'int'); + if (intval($order_to_merge) >= 0) { + // We have an order to merge, get it, and its lines + $fromElement = 'commande'; + $fromElementid = $order_to_merge; + $parent = new Commande($db); + $parent->fetch($fromElementid); + $parent->fetch_lines(); + $importLines = array(); + foreach ($parent->lines as $line) { + $importLines[] = $line->id; + } + + + // COPY OF Import action + if (!empty($importLines) && is_array($importLines) && !empty($fromElement) && ctype_alpha($fromElement) && !empty($fromElementid)) { + if ($fromElement == 'commande') { + dol_include_once('/'.$fromElement.'/class/'.$fromElement.'.class.php'); + $lineClassName = 'OrderLine'; + $parent = new Commande($db); + } elseif ($fromElement == 'propal') { + dol_include_once('/comm/'.$fromElement.'/class/'.$fromElement.'.class.php'); + $lineClassName = 'PropaleLigne'; + $parent = new Propal($db); + } + $nextRang = count($nextSituationInvoice->lines) + 1; + $importCount = 0; + $error = 0; + + // Add sub-total title + if (isset($parent)) { + $parent->fetch($fromElementid); + $label = ''; + if ($parent->ref_client) { + $label .= $parent->ref_client." - (".$parent->ref.")"; + } else { + $label .= "(".$parent->ref.")"; + } + $sub_tot = new TSubtotal(); + $sub_tot->addTitle($nextSituationInvoice, $label, 1); + $nextRang++; + } + + // Import lines + foreach ($importLines as $lineId) { + $lineId = intval($lineId); + $originLine = new $lineClassName($db); + if (intval($fromElementid) > 0 && $originLine->fetch($lineId) > 0) { + $originLine->fetch_optionals(); + $desc = $originLine->desc; + $pu_ht = $originLine->subprice; + $qty = $originLine->qty; + $txtva = $originLine->tva_tx; + $txlocaltax1 = $originLine->localtax1_tx; + $txlocaltax2 = $originLine->localtax2_tx; + $fk_product = $originLine->fk_product; + $remise_percent = $originLine->remise_percent; + $date_start = $originLine->date_start; + $date_end = $originLine->date_end; + $ventil = 0; + $info_bits = $originLine->info_bits; + $fk_remise_except = $originLine->fk_remise_except; + $price_base_type = 'HT'; + $pu_ttc = 0; + $type = $originLine->product_type; + $rang = $nextRang++; + $special_code = $originLine->special_code; + $origin = $originLine->element; + $origin_id = $originLine->id; + $fk_parent_line = 0; + $fk_fournprice = $originLine->fk_fournprice; + $pa_ht = $originLine->pa_ht; + $label = $originLine->label; + $array_options = $originLine->array_options; + if ($object->type == Facture::TYPE_SITUATION) { + $situation_percent = 0; + } else { + $situation_percent = 100; + } + $fk_prev_id = ''; + $fk_unit = $originLine->fk_unit; + $pu_ht_devise = $originLine->multicurrency_subprice; + + $res = $nextSituationInvoice->addline($desc, $pu_ht, $qty, $txtva, $txlocaltax1, $txlocaltax2, $fk_product, $remise_percent, $date_start, $date_end, $ventil, $info_bits, $fk_remise_except, $price_base_type, $pu_ttc, $type, $rang, $special_code, $origin, $origin_id, $fk_parent_line, $fk_fournprice, $pa_ht, $label, $array_options, $situation_percent, $fk_prev_id, $fk_unit, $pu_ht_devise); + + if ($res > 0) { + $importCount++; + } else { + $error++; + } + } else { + $error++; + } + } + + // Add sub-total result + if (isset($parent)) {$sub_tot->addTotal($nextSituationInvoice, $label, 1);} + + if ($error) { + setEventMessages($langs->trans('ErrorsOnXLines', $error), null, 'errors'); + } + } + } + + + // Link added order to created invoice + $nextSituationInvoice->add_object_linked($fromElement, $fromElementid, $user); + // create extrafields with data from create form $extrafields->fetch_name_optionals_label($nextSituationInvoice->table_element); $ret = $extrafields->setOptionalsFromPost(null, $nextSituationInvoice); @@ -2315,7 +2472,7 @@ } $type = $prod->type; - $fk_unit = $prod->fk_unit; + $fk_unit = (getDolGlobalInt('MAIN_EDIT_LINE_SET_UNIT_ON_EXISTING_PRODUCT') ? GETPOST('units', 'alpha') : $prod->fk_unit) ; } else { if (!empty($price_ht)) $pu_ht = price2num($price_ht, 'MU'); else $pu_ht = ''; @@ -2895,13 +3052,31 @@ if ($fromElement == 'commande') { dol_include_once('/'.$fromElement.'/class/'.$fromElement.'.class.php'); $lineClassName = 'OrderLine'; + $parent = new Commande($db); } elseif ($fromElement == 'propal') { dol_include_once('/comm/'.$fromElement.'/class/'.$fromElement.'.class.php'); $lineClassName = 'PropaleLigne'; + $parent = new Propal($db); } $nextRang = count($object->lines) + 1; $importCount = 0; $error = 0; + + // Add sub-total title + if (isset($parent)) { + $parent->fetch($fromElementid); + $label = ''; + if ($parent->ref_client) { + $label .= $parent->ref_client." - (".$parent->ref.")"; + } else { + $label .= "(".$parent->ref.")"; + } + $sub_tot = new TSubtotal(); + $sub_tot->addTitle($object, $label, 1); + $nextRang++; + } + + // Import lines foreach ($importLines as $lineId) { $lineId = intval($lineId); $originLine = new $lineClassName($db); @@ -2953,6 +3128,9 @@ } } + // Add sub-total result + if (isset($parent)) {$sub_tot->addTotal($object, $label, 1);} + if ($error) { setEventMessages($langs->trans('ErrorsOnXLines', $error), null, 'errors'); } @@ -3151,6 +3329,9 @@ $projectid = (!empty($projectid) ? $projectid : $objectsrc->fk_project); $ref_client = (!empty($objectsrc->ref_client) ? $objectsrc->ref_client : (!empty($objectsrc->ref_customer) ? $objectsrc->ref_customer : '')); + // -- Hook Couffignal + $ref_client = 'S' . $objectsrc->situation_counter + 1; + // -- // only if socid not filled else it's allready done upper if (empty($socid)) { @@ -3543,7 +3724,11 @@ function setRadioForTypeOfInvoice() { print ''; // Next situation invoice - $opt = $form->selectSituationInvoices(GETPOST('originid', 'int'), $socid); + $filt_id = 0; + if (getDolGlobalString('NEXT_SITUATION_FILTER_ON_PROJECT')) { + $filt_id = $projectid; + } + $opt = $form->selectSituationInvoices(GETPOST('originid', 'int'), $socid, $filt_id); print '
'; $tmp = 'getSelectConditionsPaiements($cond_reglement_id, 'cond_reglement_id', -1, 1, 0, 'maxwidth500 widthcentpercentminusx'); print ''; - + // Use retained warranty if (getDolGlobalString('INVOICE_USE_RETAINED_WARRANTY')) { $rwStyle = 'display:none;'; if (in_array(GETPOST('type', 'int'), $retainedWarrantyInvoiceAvailableType)) { @@ -3872,6 +4057,20 @@ function setRadioForTypeOfInvoice() { '; } + // Use prorata discount + if (getDolGlobalString('INVOICE_USE_PRORATA_DISCOUNT')) { + // Get existing rate if any + $prorata_rate = GETPOST('prorata_rate', 'float'); + if (empty($prorata_rate)) { + // On a situation, use previous situation value + if ($objectsrc instanceof Facture && !empty($objectsrc->prorata_rate)) { + $prorata_rate = $objectsrc->prorata_rate; + } + } + print '
"; print '"; } + + /* + Suggest to link any new order added to the project since the last invoice */ + + // First get the previous invoice + if ($objectsrc instanceof Facture) { + $prev_invoice = $objectsrc; + } else { + $objectsrc->fetchObjectLinked($originid, $origin, '', 'facture'); + if (isset($objectsrc->linkedObjects['facture']) && is_array($objectsrc->linkedObjects['facture']) && count($objectsrc->linkedObjects['facture']) >= 1) { + $prev_invoice = end($objectsrc->linkedObjects['facture']); + } else { + $prev_invoice = null; + } + } + + // Then print the box + // With no projectid or no prev_invoice, makes no sense + if (is_null($projectid)) { + dol_syslog('Create new invoice - COUFFIGNAL feat - Pas de chantier lié', LOG_INFO); + } elseif (is_null($prev_invoice)) { + dol_syslog('Create new invoice - COUFFIGNAL feat - Pas de facture historique', LOG_INFO); + } else { + print '"; + } + } print "
'.$langs->trans('ProrataRate').''; + print '%'; + } + // Payment mode print '
'.$langs->trans('PaymentMode').''; print img_picto('', 'bank', 'class="pictofixedwidth"'); @@ -4072,6 +4271,32 @@ function setRadioForTypeOfInvoice() { print '
'.$langs->trans('MulticurrencyAmountVAT').''.price($objectsrc->multicurrency_total_tva)."
'.$langs->trans('MulticurrencyAmountTTC').''.price($objectsrc->multicurrency_total_ttc)."
'.$langs->trans('Avenant à ajouter').''.$form->select_new_orders_in_project($projectid, $prev_invoice)."
\n"; @@ -4087,7 +4312,7 @@ function setRadioForTypeOfInvoice() { $title = $langs->trans('ProductsAndServices'); print load_fiche_titre($title); - print '
'; + print '
'; print ''; $objectsrc->printOriginLinesList('', $selectedLines); @@ -4140,7 +4365,7 @@ function setRadioForTypeOfInvoice() { // For example print 239.2 - 229.3 - 9.9; does not return 0. // $resteapayer=bcadd($object->total_ttc,$totalpaid,$conf->global->MAIN_MAX_DECIMALS_TOT); // $resteapayer=bcadd($resteapayer,$totalavoir,$conf->global->MAIN_MAX_DECIMALS_TOT); - $resteapayer = price2num($object->total_ttc - $totalpaid - $totalcreditnotes - $totaldeposits, 'MT'); + $resteapayer = price2num($object->total_ttc - $totalpaid - $totalcreditnotes - $totaldeposits - $object->prorata_discount - $object->tvaOnProrata(), 'MT'); // Multicurrency if (isModEnabled('multicurrency')) { @@ -4831,8 +5056,7 @@ function setRadioForTypeOfInvoice() { print ''; } - - + // Retained warranty if (!empty($object->retained_warranty) || getDolGlobalString('INVOICE_USE_RETAINED_WARRANTY')) { $displayWarranty = true; if (!in_array($object->type, $retainedWarrantyInvoiceAvailableType) && empty($object->retained_warranty)) { @@ -4932,6 +5156,35 @@ function setRadioForTypeOfInvoice() { } } + // Prorata discount + if (getDolGlobalString('INVOICE_USE_PRORATA_DISCOUNT')) { + // Prorata rate + print ''; + + } // Other attributes $cols = 2; @@ -4952,25 +5205,80 @@ function setRadioForTypeOfInvoice() { $sign = -1; // We invert sign for output } print ''; - // Amount HT - print ''; - print ''; - if (isModEnabled("multicurrency") && ($object->multicurrency_code && $object->multicurrency_code != $conf->currency)) { - // Multicurrency Amount HT - print ''; - } - print ''; - print ''; - // Amount VAT - print ''; - print ''; - if (isModEnabled("multicurrency") && ($object->multicurrency_code && $object->multicurrency_code != $conf->currency)) { - // Multicurrency Amount VAT - print ''; - } - print ''; + if (getDolGlobalString('INVOICE_USE_PRORATA_DISCOUNT')) { + $special_lines = $object->getExtractSpecialLines($langs); + $sum_special_lines = 0; + foreach ($special_lines as $idx => $line) { + $sum_special_lines += $line['amountHT']; + } + // Contract amount + print ''; + print ''; + print ''; + print ''; + + + // Special lines + foreach ($special_lines as $idx => $line) { + $name = $line['name']; + $total = (float) $line['amountHT']; + print ''; + print ''; + print ''; + print ''; + } + + // Amount work + print ''; + print ''; + print ''; + print ''; + + if ($object->prorata_discount > 0) { + // Amount Prorata + print ''; + print ''; + print ''; + print ''; + } + + // Amount HT + print ''; + print ''; + print ''; + print ''; + + // Amount VAT + print ''; + print ''; + print ''; + print ''; + + + } else { + // Amount HT + print ''; + print ''; + print ''; + if (isModEnabled("multicurrency") && ($object->multicurrency_code && $object->multicurrency_code != $conf->currency)) { + // Multicurrency Amount HT + print ''; + } + print ''; + + print ''; + // Amount VAT + print ''; + print ''; + if (isModEnabled("multicurrency") && ($object->multicurrency_code && $object->multicurrency_code != $conf->currency)) { + // Multicurrency Amount VAT + print ''; + } + print ''; + } + // Amount Local Taxes if (($mysoc->localtax1_assuj == "1" && $mysoc->useLocalTax(1)) || $object->total_localtax1 != 0) { print ''; @@ -5052,7 +5360,7 @@ function js_recalculate_revenuestamp(){ print ''; // Amount TTC print ''; - print ''; + print ''; if (isModEnabled("multicurrency") && ($object->multicurrency_code && $object->multicurrency_code != $conf->currency)) { // Multicurrency Amount TTC print ''; @@ -5095,7 +5403,7 @@ function js_recalculate_revenuestamp(){ print ''; print ''; print ''; - + print ''; if (isModEnabled("banque")) { print ''; } @@ -5120,6 +5428,7 @@ function js_recalculate_revenuestamp(){ print ''; print ''; print ''; + print ''; if (isModEnabled("banque")) { print ''; } @@ -5140,6 +5449,7 @@ function js_recalculate_revenuestamp(){ print ''; print ''; print ''; + print ''; if (isModEnabled("banque")) { print ''; } @@ -5163,6 +5473,7 @@ function js_recalculate_revenuestamp(){ $i++; } print ''; + print ''; if (isModEnabled("banque")) { print ''; } @@ -5197,6 +5508,7 @@ function js_recalculate_revenuestamp(){ print ''; print ''; print ''; + print ''; if (isModEnabled("banque")) { print ''; } @@ -5211,6 +5523,7 @@ function js_recalculate_revenuestamp(){ print ''; print ''; + print ''; if (isModEnabled("banque")) { print ''; } @@ -5220,6 +5533,19 @@ function js_recalculate_revenuestamp(){ print ''; } + // New line to recap the total price of the situation invoice' series + print ''; + print ''; + print ''; + print ''; + if (isModEnabled("banque")) { + print ''; + } + print ''; + print ''; + print ''; + print ''; + print '
'; + print ''; + if ($action != 'editproratarate' && $usercancreate && $object->statut == Facture::STATUS_DRAFT) { + print ''; + } + + print '
'; + print $langs->trans('ProrataRate'); + print 'id.'">'.img_edit($langs->trans('SetProrataRate'), 1).'
'; + print '
'; + if ($action == 'editproratarate' && $object->statut == Facture::STATUS_DRAFT) { + print '
'; + print ''; + print ''; + print ''; + print '% '; + print '( €)'; + print ''; + print '
'; + } else { + $rate = $object->prorata_rate == -1 ? '-' : price($object->prorata_rate).'%'; + print $rate.' ('.price(round($object->prorata_discount, 2)).'€)'; + } + print '
' . $langs->trans('AmountHT') . '' . price($sign * $object->total_ht, '', $langs, 0, -1, -1, $conf->currency) . '' . price($sign * $object->multicurrency_total_ht, '', $langs, 0, -1, -1, $object->multicurrency_code) . '
' . $langs->trans('AmountVAT') . '' . price($sign * $object->total_tva, '', $langs, 0, -1, -1, $conf->currency) . '' . price($sign * $object->multicurrency_total_tva, '', $langs, 0, -1, -1, $object->multicurrency_code) . '
' . $langs->trans('MontantMarché') . '' . price($sign * $object->total_ht - $sum_special_lines, '', $langs, 0, -1, -1, $conf->currency) . '
' . $langs->trans($name) . '' . price($total, '', $langs, 0, -1, -1, $conf->currency) . '
' . $langs->trans('MontantTravaux') . '' . price($sign * $object->total_ht, '', $langs, 0, -1, -1, $conf->currency) . '
' . $langs->trans('ProrataRate') . '' . price(-round($object->prorata_discount, 2), '', $langs, 0, -1, -1, $conf->currency) . '
' . $langs->trans('AmountHT') . '' . price($sign * round($object->total_ht - $object->prorata_discount, 2), '', $langs, 0, -1, -1, $conf->currency) . '
' . $langs->trans('AmountVAT') . '' . price(round($sign * $object->total_tva - $object->tvaOnProrata(), 2), '', $langs, 0, -1, -1, $conf->currency) . '
' . $langs->trans('AmountHT') . '' . price($sign * $object->total_ht, '', $langs, 0, -1, -1, $conf->currency) . '' . price($sign * $object->multicurrency_total_ht, '', $langs, 0, -1, -1, $object->multicurrency_code) . '
' . $langs->trans('AmountVAT') . '' . price($sign * $object->total_tva - $object->prorata_discount, '', $langs, 0, -1, -1, $conf->currency) . '' . price($sign * $object->multicurrency_total_tva, '', $langs, 0, -1, -1, $object->multicurrency_code) . '
' . $langs->trans('AmountTTC') . '' . price($sign * $object->total_ttc, '', $langs, 0, -1, -1, $conf->currency) . '' . price(round($object->total_ttc - $object->prorata_discount - $object->tvaOnProrata(), 2), '', $langs, 0, -1, -1, $object->multicurrency_code) . '' . price($sign * $object->multicurrency_total_ttc, '', $langs, 0, -1, -1, $object->multicurrency_code) . ''.$langs->trans('ListOfSituationInvoices').''.$langs->trans('Situation').''.$langs->trans('SituationInvoiceProgressColTitle').''.$prev_invoice->getNomUrl(1).''.(($prev_invoice->type == Facture::TYPE_CREDIT_NOTE) ? $langs->trans('situationInvoiceShortcode_AS') : $langs->trans('situationInvoiceShortcode_S')).$prev_invoice->situation_counter.''.$prev_invoice->computeMarginalProgress().'%'.$object->getNomUrl(1).''.(($object->type == Facture::TYPE_CREDIT_NOTE) ? $langs->trans('situationInvoiceShortcode_AS') : $langs->trans('situationInvoiceShortcode_S')).$object->situation_counter.''.$object->computeMarginalProgress().'%'.$object->computeGlobalProgress().'%'.$next_invoice->getNomUrl(1).''.(($next_invoice->type == Facture::TYPE_CREDIT_NOTE) ? $langs->trans('situationInvoiceShortcode_AS') : $langs->trans('situationInvoiceShortcode_S')).$next_invoice->situation_counter.''.$next_invoice->computeMarginalProgress().'%
'.$langs->trans('SituationSerieTotal').''.price($object->getLastSituationCompletePrice()).' 
'; } @@ -5231,7 +5557,7 @@ function js_recalculate_revenuestamp(){ // List of payments already done print ''; - print '
'; + print '
'; print ''; print ''; @@ -5341,24 +5667,42 @@ function js_recalculate_revenuestamp(){ } print ''; + // Billed + print ''; + } else { + print ''; + } + $resteapayeraffiche = $resteapayer; $cssforamountpaymentcomplete = 'amountpaymentcomplete'; // Loop on each credit note or deposit amount applied $creditnoteamount = 0; $depositamount = 0; + $compensated_amount = 0; $sql = "SELECT re.rowid, re.amount_ht, re.amount_tva, re.amount_ttc,"; - $sql .= " re.description, re.fk_facture_source"; + $sql .= " re.description, re.fk_facture_source, re.fk_invoice_supplier_source"; $sql .= " FROM ".MAIN_DB_PREFIX."societe_remise_except as re"; $sql .= " WHERE fk_facture = ".((int) $object->id); $resql = $db->query($sql); if ($resql) { $num = $db->num_rows($resql); $i = 0; - $invoice = new Facture($db); while ($i < $num) { $obj = $db->fetch_object($resql); - $invoice->fetch($obj->fk_facture_source); + if (!empty($obj->fk_invoice_supplier_source)) { + require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php'; + $invoice = new FactureFournisseur($db); + $id = $obj->fk_invoice_supplier_source; + } elseif (!empty($obj->fk_facture_source)) { + $invoice = new Facture($db); + $id = $obj->fk_facture_source; + } + $invoice->fetch($id); print ''; @@ -5383,6 +5730,9 @@ function js_recalculate_revenuestamp(){ if ($invoice->type == Facture::TYPE_DEPOSIT) { $depositamount += $obj->amount_ttc; } + if (class_exists('FactureFournisseur') && $invoice->type == FactureFournisseur::TYPE_STANDARD) { + $compensated_amount += $obj->amount_ttc; + } } } else { dol_print_error($db); @@ -5394,7 +5744,7 @@ function js_recalculate_revenuestamp(){ print ''; print $form->textwithpicto($langs->trans("Discount"), $langs->trans("HelpEscompte"), - 1); print ''; - print ''; + print ''; $resteapayeraffiche = 0; $cssforamountpaymentcomplete = 'amountpaymentneutral'; } @@ -5404,7 +5754,7 @@ function js_recalculate_revenuestamp(){ print ''; print $form->textwithpicto($langs->trans("Abandoned"), $langs->trans("HelpAbandonBadCustomer"), - 1); print ''; - print ''; + print ''; // $resteapayeraffiche=0; $cssforamountpaymentcomplete = 'amountpaymentneutral'; } @@ -5414,7 +5764,7 @@ function js_recalculate_revenuestamp(){ print ''; print $form->textwithpicto($langs->trans("ProductReturned"), $langs->trans("HelpAbandonProductReturned"), - 1); print ''; - print ''; + print ''; $resteapayeraffiche = 0; $cssforamountpaymentcomplete = 'amountpaymentneutral'; } @@ -5428,17 +5778,12 @@ function js_recalculate_revenuestamp(){ print ''; print $form->textwithpicto($langs->trans("Abandoned"), $text, - 1); print ''; - print ''; + print ''; $resteapayeraffiche = 0; $cssforamountpaymentcomplete = 'amountpaymentneutral'; } - // Billed - print ''; - // Remainder to pay + // Remaining to pay print '
'.price($totalpaid).' 
'; + print ''; + print $langs->trans("Billed"); + if (getDolGlobalString('INVOICE_USE_SITUATION')) { + print ''.price(round($object->total_ttc - $object->prorata_discount - $object->tvaOnProrata(), 2)).' 
'.price($object->total_ttc).' 
'; print ''; if ($invoice->type == Facture::TYPE_CREDIT_NOTE) { @@ -5367,6 +5711,9 @@ function js_recalculate_revenuestamp(){ if ($invoice->type == Facture::TYPE_DEPOSIT) { print $langs->trans("Deposit").' '; } + if (class_exists('FactureFournisseur') && $invoice->type == FactureFournisseur::TYPE_STANDARD) { + print $langs->trans("CompensatedDebt").' '; + } print $invoice->getNomUrl(0); print ''; print ''.price(price2num($object->total_ttc - $creditnoteamount - $depositamount - $totalpaid, 'MT')).' 
'.price(price2num($object->total_ttc - $creditnoteamount - $depositamount - $compensated_amount - $totalpaid, 'MT')).' 
'.price(price2num($object->total_ttc - $creditnoteamount - $depositamount - $totalpaid, 'MT')).' 
'.price(price2num($object->total_ttc - $creditnoteamount - $depositamount - $compensated_amount - $totalpaid, 'MT')).' 
'.price(price2num($object->total_ttc - $creditnoteamount - $depositamount - $totalpaid, 'MT')).' 
'.price(price2num($object->total_ttc - $creditnoteamount - $depositamount - $compensated_amount - $totalpaid, 'MT')).' 
'.price(price2num($object->total_ttc - $creditnoteamount - $depositamount - $totalpaid, 'MT')).' 
'.price(price2num($object->total_ttc - $creditnoteamount - $depositamount - $compensated_amount - $totalpaid, 'MT')).' 
'; - print ''; - print $langs->trans("Billed"); - print ''.price($object->total_ttc).' 
'; print ''; print $langs->trans('RemainderToPay'); @@ -5465,7 +5810,7 @@ function js_recalculate_revenuestamp(){ } // Retained warranty : usualy use on construction industry - if (!empty($object->situation_final) && !empty($object->retained_warranty) && $displayWarranty) { + if (!empty($object->retained_warranty) && $displayWarranty) { // !empty($object->situation_final) && // Billed - retained warranty if ($object->type == Facture::TYPE_SITUATION) { $retainedWarranty = $total_global_ttc * $object->retained_warranty / 100; @@ -5561,7 +5906,7 @@ function js_recalculate_revenuestamp(){ if (getDolGlobalString('INVOICE_USE_SITUATION')) { if ($object->situation_cycle_ref && $object->statut == 0) { print ''."\n"; - print '
'; + print '
'; print '
'; print ''; @@ -5613,7 +5958,7 @@ function js_recalculate_revenuestamp(){ include DOL_DOCUMENT_ROOT.'/core/tpl/ajaxrow.tpl.php'; } - print '
'; + print '
'; print ''; // Show object lines @@ -5841,7 +6186,7 @@ function js_recalculate_revenuestamp(){ // Classify 'closed not completely paid' (possible if validated and not yet filed paid) if ($object->statut == Facture::STATUS_VALIDATED && $object->paye == 0 && $resteapayer > 0 && (!getDolGlobalString('INVOICE_CAN_SET_PAID_EVEN_IF_PARTIALLY_PAID') || $resteapayer != $object->total_ttc) && $usercanissuepayment) { - if ($totalpaid > 0 || $totalcreditnotes > 0) { + if ($totalpaid > 0 || $totalcreditnotes > 0 || $compensated_amount > 0) { // If one payment or one credit note was linked to this invoice print ''.$langs->trans('ClassifyPaidPartially').''; } else { @@ -5990,8 +6335,9 @@ function js_recalculate_revenuestamp(){ $somethingshown = $formfile->numoffiles; + // Show links to link elements - $linktoelem = $form->showLinkToObjectBlock($object, null, array('invoice')); + $linktoelem = $form->showLinkToObjectBlock($object, null, array('invoice'), $same_project_filter); $compatibleImportElementsList = false; if ($usercancreate diff --git a/htdocs/compta/facture/class/facture.class.php b/htdocs/compta/facture/class/facture.class.php index bf01e18bc3fcf..ac281e02defbc 100644 --- a/htdocs/compta/facture/class/facture.class.php +++ b/htdocs/compta/facture/class/facture.class.php @@ -1,24 +1,24 @@ - * Copyright (C) 2004-2013 Laurent Destailleur - * Copyright (C) 2004 Sebastien Di Cintio - * Copyright (C) 2004 Benoit Mortier - * Copyright (C) 2005 Marc Barilley / Ocebo - * Copyright (C) 2005-2014 Regis Houssin - * Copyright (C) 2006 Andre Cianfarani - * Copyright (C) 2007 Franky Van Liedekerke - * Copyright (C) 2010-2020 Juanjo Menent - * Copyright (C) 2012-2014 Christophe Battarel - * Copyright (C) 2012-2015 Marcos García - * Copyright (C) 2012 Cédric Salvador - * Copyright (C) 2012-2014 Raphaël Doursenaud - * Copyright (C) 2013 Cedric Gross - * Copyright (C) 2013 Florian Henry - * Copyright (C) 2016-2022 Ferran Marcet +/* Copyright (C) 2002-2007 Rodolphe Quiedeville + * Copyright (C) 2004-2013 Laurent Destailleur + * Copyright (C) 2004 Sebastien Di Cintio + * Copyright (C) 2004 Benoit Mortier + * Copyright (C) 2005 Marc Barilley / Ocebo + * Copyright (C) 2005-2014 Regis Houssin + * Copyright (C) 2006 Andre Cianfarani + * Copyright (C) 2007 Franky Van Liedekerke + * Copyright (C) 2010-2020 Juanjo Menent + * Copyright (C) 2012-2014 Christophe Battarel + * Copyright (C) 2012-2015 Marcos García + * Copyright (C) 2012 Cédric Salvador + * Copyright (C) 2012-2014 Raphaël Doursenaud + * Copyright (C) 2013 Cedric Gross + * Copyright (C) 2013 Florian Henry + * Copyright (C) 2016-2022 Ferran Marcet * Copyright (C) 2018-2022 Alexandre Spangaro - * Copyright (C) 2018 Nicolas ZABOURI - * Copyright (C) 2022 Sylvain Legrand - * Copyright (C) 2023 Gauthier VERDOL + * Copyright (C) 2018 Nicolas ZABOURI + * Copyright (C) 2022 Sylvain Legrand + * Copyright (C) 2023 Gauthier VERDOL * Copyright (C) 2023 Nick Fragoulis * * This program is free software; you can redistribute it and/or modify @@ -36,9 +36,9 @@ */ /** - * \file htdocs/compta/facture/class/facture.class.php - * \ingroup facture - * \brief File of class to manage invoices + * \file htdocs/compta/facture/class/facture.class.php + * \ingroup facture + * \brief File of class to manage invoices */ require_once DOL_DOCUMENT_ROOT.'/core/class/commoninvoice.class.php'; @@ -71,7 +71,7 @@ class Facture extends CommonInvoice public $table_element = 'facture'; /** - * @var string Name of subtable line + * @var string Name of subtable line */ public $table_element_line = 'facturedet'; @@ -162,7 +162,13 @@ class Facture extends CommonInvoice */ public $ref_customer; - public $total_ht; + public $total_ht; /* To be changed with PHP8.4 support { + set { + // Get rate before changing total to update discount with new total + $this->prorata_discount = $value * $this->prorata_rate; // Silently update prorata discount on total change + $this->total_ht = $value; + } + }*/ public $total_tva; public $total_localtax1; public $total_localtax2; @@ -255,6 +261,32 @@ class Facture extends CommonInvoice */ public $retained_warranty_fk_cond_reglement; + /** + * @var double amount to be retained on payment and used as a charge + */ + public $prorata_discount = 0; + + /** + * @var double percent (0 to 100) to be retained on payment and used as a charge + * WARNING: DO NOT SET DIRECTlY, until PHP8.4 property hooks are in place + */ + public $prorata_rate; + + /** + * Virtual property + * var percent of prorata_discount compared to total_ht + */ + /*public double $prorata_rate; For PHP8.4 = { + get { + if (isset($this->total_ht) && $this->total_ht != 0) { + return $this->$prorata_discount / $this->total_ht; + } else { return -1; } + } + set { + $this->prorata_discount = $value * $this->total_ht; + } + }*/ + /** * @var int availabilty ID */ @@ -275,7 +307,7 @@ class Facture extends CommonInvoice /** * 'type' if the field format ('integer', 'integer:ObjectClass:PathToClass[:AddCreateButtonOrNot[:Filter]]', 'varchar(x)', 'double(24,8)', 'real', 'price', 'text', 'html', 'date', 'datetime', 'timestamp', 'duration', 'mail', 'phone', 'url', 'password') - * Note: Filter can be a string like "(t.ref:like:'SO-%') or (t.date_creation:<:'20160101') or (t.nature:is:NULL)" + * Note: Filter can be a string like "(t.ref:like:'SO-%') or (t.date_creation:<:'20160101') or (t.nature:is:NULL)" * 'label' the translation key. * 'enabled' is a condition when the field must be managed. * 'position' is the sort order of field. @@ -450,7 +482,7 @@ public function __construct(DoliDB $db) * Note: this->ref can be set or empty. If empty, we will use "(PROV999)" * Note: this->fac_rec must be set to create invoice from a recurring invoice * - * @param User $user Object user that create + * @param User $user Object user that create * @param int $notrigger 1=Does not execute triggers, 0 otherwise * @param int $forceduedate If set, do not recalculate due date from payment condition but force it with value * @return int Return integer <0 if KO, >0 if OK @@ -543,24 +575,24 @@ public function create(User $user, $notrigger = 0, $forceduedate = 0) if (!empty($_facrec->frequency)) { // Invoice are created on same thirdparty than template when there is a recurrence, but not necessarly when there is no recurrence. $this->socid = $_facrec->socid; } - $this->entity = $_facrec->entity; // Invoice created in same entity than template + $this->entity = $_facrec->entity; // Invoice created in same entity than template // Fields coming from GUI (priority on template). TODO Value of template should be used as default value on GUI so we can use here always value from GUI - $this->fk_project = GETPOST('projectid', 'int') > 0 ? ((int) GETPOST('projectid', 'int')) : $_facrec->fk_project; - $this->note_public = GETPOSTISSET('note_public') ? GETPOST('note_public', 'restricthtml') : $_facrec->note_public; - $this->note_private = GETPOSTISSET('note_private') ? GETPOST('note_private', 'restricthtml') : $_facrec->note_private; + $this->fk_project = GETPOST('projectid', 'int') > 0 ? ((int) GETPOST('projectid', 'int')) : $_facrec->fk_project; + $this->note_public = GETPOSTISSET('note_public') ? GETPOST('note_public', 'restricthtml') : $_facrec->note_public; + $this->note_private = GETPOSTISSET('note_private') ? GETPOST('note_private', 'restricthtml') : $_facrec->note_private; $this->model_pdf = GETPOSTISSET('model') ? GETPOST('model', 'alpha') : $_facrec->model_pdf; $this->cond_reglement_id = GETPOST('cond_reglement_id', 'int') > 0 ? ((int) GETPOST('cond_reglement_id', 'int')) : $_facrec->cond_reglement_id; $this->mode_reglement_id = GETPOST('mode_reglement_id', 'int') > 0 ? ((int) GETPOST('mode_reglement_id', 'int')) : $_facrec->mode_reglement_id; - $this->fk_account = GETPOST('fk_account') > 0 ? ((int) GETPOST('fk_account')) : $_facrec->fk_account; + $this->fk_account = GETPOST('fk_account') > 0 ? ((int) GETPOST('fk_account')) : $_facrec->fk_account; // Set here to have this defined for substitution into notes, should be recalculated after adding lines to get same result - $this->total_ht = $_facrec->total_ht; - $this->total_ttc = $_facrec->total_ttc; + $this->total_ht = $_facrec->total_ht; + $this->total_ttc = $_facrec->total_ttc; // Fields always coming from template - //$this->remise_absolue = $_facrec->remise_absolue; - //$this->remise_percent = $_facrec->remise_percent; // TODO deprecated + //$this->remise_absolue = $_facrec->remise_absolue; + //$this->remise_percent = $_facrec->remise_percent; // TODO deprecated $this->fk_incoterms = $_facrec->fk_incoterms; $this->location_incoterms = $_facrec->location_incoterms; @@ -678,6 +710,7 @@ public function create(User $user, $notrigger = 0, $forceduedate = 0) $sql .= ", retained_warranty"; $sql .= ", retained_warranty_date_limit"; $sql .= ", retained_warranty_fk_cond_reglement"; + $sql .= ", prorata_discount"; $sql .= ")"; $sql .= " VALUES ("; $sql .= "'(PROV)'"; @@ -714,6 +747,7 @@ public function create(User $user, $notrigger = 0, $forceduedate = 0) $sql .= ", ".(empty($this->retained_warranty) ? "0" : $this->db->escape($this->retained_warranty)); $sql .= ", ".(!empty($this->retained_warranty_date_limit) ? "'".$this->db->idate($this->retained_warranty_date_limit)."'" : 'NULL'); $sql .= ", ".(int) $this->retained_warranty_fk_cond_reglement; + $sql .= ", ".(double) $this->prorata_discount; $sql .= ")"; $resql = $this->db->query($sql); @@ -736,7 +770,7 @@ public function create(User $user, $notrigger = 0, $forceduedate = 0) // Add object linked if (!$error && $this->id && !empty($this->linked_objects) && is_array($this->linked_objects)) { foreach ($this->linked_objects as $origin => $tmp_origin_id) { - if (is_array($tmp_origin_id)) { // New behaviour, if linked_object can have several links per type, so is something like array('contract'=>array(id1, id2, ...)) + if (is_array($tmp_origin_id)) { // New behaviour, if linked_object can have several links per type, so is something like array('contract'=>array(id1, id2, ...)) foreach ($tmp_origin_id as $origin_id) { $ret = $this->add_object_linked($origin, $origin_id); if (!$ret) { @@ -759,7 +793,7 @@ public function create(User $user, $notrigger = 0, $forceduedate = 0) if (!$error && $this->id && getDolGlobalString('MAIN_PROPAGATE_CONTACTS_FROM_ORIGIN') && !empty($this->origin) && !empty($this->origin_id)) { // Get contact from origin object $originforcontact = $this->origin; $originidforcontact = $this->origin_id; - if ($originforcontact == 'shipping') { // shipment and order share the same contacts. If creating from shipment we take data of order + if ($originforcontact == 'shipping') { // shipment and order share the same contacts. If creating from shipment we take data of order require_once DOL_DOCUMENT_ROOT.'/expedition/class/expedition.class.php'; $exp = new Expedition($this->db); $exp->fetch($this->origin_id); @@ -1127,7 +1161,7 @@ public function create(User $user, $notrigger = 0, $forceduedate = 0) /** * Create a new invoice in database from current invoice * - * @param User $user Object user that ask creation + * @param User $user Object user that ask creation * @param int $invertdetail Reverse sign of amounts for lines * @return int Return integer <0 if KO, >0 if OK */ @@ -1151,24 +1185,24 @@ public function createFromCurrent(User $user, $invertdetail = 0) } $facture->fk_facture_source = $this->fk_facture_source; - $facture->type = $this->type; + $facture->type = $this->type; $facture->subtype = $this->subtype; - $facture->socid = $this->socid; - $facture->date = $this->date; + $facture->socid = $this->socid; + $facture->date = $this->date; $facture->date_pointoftax = $this->date_pointoftax; - $facture->note_public = $this->note_public; - $facture->note_private = $this->note_private; - $facture->ref_client = $this->ref_client; - $facture->model_pdf = $this->model_pdf; - $facture->fk_project = $this->fk_project; + $facture->note_public = $this->note_public; + $facture->note_private = $this->note_private; + $facture->ref_client = $this->ref_client; + $facture->model_pdf = $this->model_pdf; + $facture->fk_project = $this->fk_project; $facture->cond_reglement_id = $this->cond_reglement_id; $facture->mode_reglement_id = $this->mode_reglement_id; - //$facture->remise_absolue = $this->remise_absolue; - //$facture->remise_percent = $this->remise_percent; // TODO deprecated + //$facture->remise_absolue = $this->remise_absolue; + //$facture->remise_percent = $this->remise_percent; // TODO deprecated - $facture->origin = $this->origin; - $facture->origin_id = $this->origin_id; - $facture->fk_account = $this->fk_account; + $facture->origin = $this->origin; + $facture->origin_id = $this->origin_id; + $facture->fk_account = $this->fk_account; $facture->lines = $this->lines; // Array of lines of invoice $facture->situation_counter = $this->situation_counter; @@ -1179,6 +1213,8 @@ public function createFromCurrent(User $user, $invertdetail = 0) $facture->retained_warranty_fk_cond_reglement = $this->retained_warranty_fk_cond_reglement; $facture->retained_warranty_date_limit = $this->retained_warranty_date_limit; + $facture->prorata_discount = $this->prorata_discount; + $facture->fk_user_author = $user->id; @@ -1221,9 +1257,9 @@ public function createFromCurrent(User $user, $invertdetail = 0) /** * Load an object from its id and create a new one in database * - * @param User $user User that clone - * @param int $fromid Id of object to clone - * @return int New id of clone + * @param User $user User that clone + * @param int $fromid Id of object to clone + * @return int New id of clone */ public function createFromClone(User $user, $fromid = 0) { @@ -1260,11 +1296,11 @@ public function createFromClone(User $user, $fromid = 0) $object->status = self::STATUS_DRAFT; // Clear fields - $object->date = (empty($this->date) ? dol_now() : $this->date); + $object->date = (empty($this->date) ? dol_now() : $this->date); $object->user_creation_id = $user->id; $object->user_validation_id = null; - $object->fk_user_author = $user->id; - $object->fk_user_valid = null; + $object->fk_user_author = $user->id; + $object->fk_user_valid = null; $object->fk_facture_source = 0; $object->fk_fac_rec_source = null; $object->date_creation = ''; @@ -1364,9 +1400,9 @@ public function createFromClone(User $user, $fromid = 0) /** * Load an object from an order and create a new invoice into database * - * @param Object $object Object source + * @param Object $object Object source * @param User $user Object user - * @return int Return integer <0 if KO, 0 if nothing done, 1 if OK + * @return int Return integer <0 if KO, 0 if nothing done, 1 if OK */ public function createFromOrder($object, User $user) { @@ -1429,17 +1465,17 @@ public function createFromOrder($object, User $user) $this->lines[$i] = $line; } - $this->socid = $object->socid; - $this->fk_project = $object->fk_project; + $this->socid = $object->socid; + $this->fk_project = $object->fk_project; $this->fk_account = $object->fk_account; - $this->cond_reglement_id = $object->cond_reglement_id; - $this->mode_reglement_id = $object->mode_reglement_id; - $this->availability_id = $object->availability_id; - $this->demand_reason_id = $object->demand_reason_id; - $this->delivery_date = $object->delivery_date; + $this->cond_reglement_id = $object->cond_reglement_id; + $this->mode_reglement_id = $object->mode_reglement_id; + $this->availability_id = $object->availability_id; + $this->demand_reason_id = $object->demand_reason_id; + $this->delivery_date = $object->delivery_date; $this->fk_delivery_address = $object->fk_delivery_address; // deprecated - $this->contact_id = $object->contact_id; - $this->ref_client = $object->ref_client; + $this->contact_id = $object->contact_id; + $this->ref_client = $object->ref_client; if (!getDolGlobalString('MAIN_DISABLE_PROPAGATE_NOTES_FROM_ORIGIN')) { $this->note_private = $object->note_private; @@ -1493,10 +1529,10 @@ public function createFromOrder($object, User $user) /** * Load an object from an order and create a new invoice into database * - * @param Object $object Object source + * @param Object $object Object source * @param User $user Object user * @param array $lines Ids of lines to use for invoice. If empty, all lines will be used. - * @return int Return integer <0 if KO, 0 if nothing done, 1 if OK + * @return int Return integer <0 if KO, 0 if nothing done, 1 if OK */ public function createFromContract($object, User $user, $lines = array()) { @@ -1564,17 +1600,17 @@ public function createFromContract($object, User $user, $lines = array()) $this->lines[$i] = $line; } - $this->socid = $object->socid; - $this->fk_project = $object->fk_project; + $this->socid = $object->socid; + $this->fk_project = $object->fk_project; $this->fk_account = $object->fk_account; - $this->cond_reglement_id = $object->cond_reglement_id; - $this->mode_reglement_id = $object->mode_reglement_id; - $this->availability_id = $object->availability_id; - $this->demand_reason_id = $object->demand_reason_id; - $this->delivery_date = $object->delivery_date; + $this->cond_reglement_id = $object->cond_reglement_id; + $this->mode_reglement_id = $object->mode_reglement_id; + $this->availability_id = $object->availability_id; + $this->demand_reason_id = $object->demand_reason_id; + $this->delivery_date = $object->delivery_date; $this->fk_delivery_address = $object->fk_delivery_address; // deprecated - $this->contact_id = $object->contact_id; - $this->ref_client = $object->ref_client; + $this->contact_id = $object->contact_id; + $this->ref_client = $object->ref_client; if (!getDolGlobalString('MAIN_DISABLE_PROPAGATE_NOTES_FROM_ORIGIN')) { $this->note_private = $object->note_private; @@ -1983,16 +2019,16 @@ public function getTooltipContentArray($params) /** * Return clicable link of object (with eventually picto) * - * @param int $withpicto Add picto into link - * @param string $option Where point the link - * @param int $max Maxlength of ref - * @param int $short 1=Return just URL - * @param string $moretitle Add more text to title tooltip + * @param int $withpicto Add picto into link + * @param string $option Where point the link + * @param int $max Maxlength of ref + * @param int $short 1=Return just URL + * @param string $moretitle Add more text to title tooltip * @param int $notooltip 1=Disable tooltip - * @param int $addlinktonotes 1=Add link to notes - * @param int $save_lastsearch_value -1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values whenclicking - * @param string $target Target of link ('', '_self', '_blank', '_parent', '_backoffice', ...) - * @return string String with URL + * @param int $addlinktonotes 1=Add link to notes + * @param int $save_lastsearch_value -1=Auto, 0=No save of lastsearch_values when clicking, 1=Save lastsearch_values whenclicking + * @param string $target Target of link ('', '_self', '_blank', '_parent', '_backoffice', ...) + * @return string String with URL */ public function getNomUrl($withpicto = 0, $option = '', $max = 0, $short = 0, $moretitle = '', $notooltip = 0, $addlinktonotes = 0, $save_lastsearch_value = -1, $target = '') { @@ -2114,12 +2150,12 @@ public function getNomUrl($withpicto = 0, $option = '', $max = 0, $short = 0, $m /** * Get object from database. Get also lines. * - * @param int $rowid Id of object to load + * @param int $rowid Id of object to load * @param string $ref Reference of invoice * @param string $ref_ext External reference of invoice * @param int $notused Not used * @param bool $fetch_situation Load also the previous and next situation invoice into $tab_previous_situation_invoice and $tab_next_situation_invoice - * @return int >0 if OK, <0 if KO, 0 if not found + * @return int >0 if OK, <0 if KO, 0 if not found */ public function fetch($rowid, $ref = '', $ref_ext = '', $notused = 0, $fetch_situation = false) { @@ -2146,6 +2182,7 @@ public function fetch($rowid, $ref = '', $ref_ext = '', $notused = 0, $fetch_sit $sql .= ', f.module_source, f.pos_source'; $sql .= ", i.libelle as label_incoterms"; $sql .= ", f.retained_warranty as retained_warranty, f.retained_warranty_date_limit as retained_warranty_date_limit, f.retained_warranty_fk_cond_reglement as retained_warranty_fk_cond_reglement"; + $sql .= ", f.prorata_discount"; $sql .= ' FROM '.MAIN_DB_PREFIX.'facture as f'; $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_payment_term as c ON f.fk_cond_reglement = c.rowid'; $sql .= ' LEFT JOIN '.MAIN_DB_PREFIX.'c_paiement as p ON f.fk_mode_reglement = p.id'; @@ -2180,17 +2217,17 @@ public function fetch($rowid, $ref = '', $ref_ext = '', $notused = 0, $fetch_sit $this->subtype = $obj->subtype; $this->date = $this->db->jdate($obj->df); $this->date_pointoftax = $this->db->jdate($obj->date_pointoftax); - $this->date_creation = $this->db->jdate($obj->datec); + $this->date_creation = $this->db->jdate($obj->datec); $this->date_validation = $this->db->jdate($obj->datev); - $this->date_modification = $this->db->jdate($obj->datem); - $this->datem = $this->db->jdate($obj->datem); + $this->date_modification = $this->db->jdate($obj->datem); + $this->datem = $this->db->jdate($obj->datem); $this->total_ht = $obj->total_ht; $this->total_tva = $obj->total_tva; $this->total_localtax1 = $obj->localtax1; $this->total_localtax2 = $obj->localtax2; $this->total_ttc = $obj->total_ttc; - $this->revenuestamp = $obj->revenuestamp; - $this->paye = $obj->paye; + $this->revenuestamp = $obj->revenuestamp; + $this->paye = $obj->paye; $this->close_code = $obj->close_code; $this->close_note = $obj->close_note; @@ -2217,25 +2254,27 @@ public function fetch($rowid, $ref = '', $ref_ext = '', $notused = 0, $fetch_sit $this->note = $obj->note_private; // deprecated $this->note_private = $obj->note_private; $this->note_public = $obj->note_public; - $this->user_creation_id = $obj->fk_user_author; + $this->user_creation_id = $obj->fk_user_author; $this->user_validation_id = $obj->fk_user_valid; $this->user_modification_id = $obj->fk_user_modif; - $this->fk_user_author = $obj->fk_user_author; - $this->fk_user_valid = $obj->fk_user_valid; - $this->fk_user_modif = $obj->fk_user_modif; + $this->fk_user_author = $obj->fk_user_author; + $this->fk_user_valid = $obj->fk_user_valid; + $this->fk_user_modif = $obj->fk_user_modif; $this->model_pdf = $obj->model_pdf; $this->last_main_doc = $obj->last_main_doc; $this->situation_cycle_ref = $obj->situation_cycle_ref; - $this->situation_counter = $obj->situation_counter; - $this->situation_final = $obj->situation_final; - $this->retained_warranty = $obj->retained_warranty; - $this->retained_warranty_date_limit = $this->db->jdate($obj->retained_warranty_date_limit); + $this->situation_counter = $obj->situation_counter; + $this->situation_final = $obj->situation_final; + $this->retained_warranty = $obj->retained_warranty; + $this->retained_warranty_date_limit = $this->db->jdate($obj->retained_warranty_date_limit); $this->retained_warranty_fk_cond_reglement = $obj->retained_warranty_fk_cond_reglement; + $this->prorata_discount = $obj->prorata_discount; + $this->prorata_rate = $obj->total_ht != 0 ? $obj->prorata_discount / $obj->total_ht * 100 : 0; $this->extraparams = !empty($obj->extraparams) ? (array) json_decode($obj->extraparams, true) : array(); //Incoterms - $this->fk_incoterms = $obj->fk_incoterms; + $this->fk_incoterms = $obj->fk_incoterms; $this->location_incoterms = $obj->location_incoterms; $this->label_incoterms = $obj->label_incoterms; @@ -2290,7 +2329,7 @@ public function fetch($rowid, $ref = '', $ref_ext = '', $notused = 0, $fetch_sit * @param int $only_product Return only physical products * @param int $loadalsotranslation Return translation for products * - * @return int 1 if OK, < 0 if KO + * @return int 1 if OK, < 0 if KO */ public function fetch_lines($only_product = 0, $loadalsotranslation = 0) { @@ -2320,41 +2359,41 @@ public function fetch_lines($only_product = 0, $loadalsotranslation = 0) $objp = $this->db->fetch_object($result); $line = new FactureLigne($this->db); - $line->id = $objp->rowid; + $line->id = $objp->rowid; $line->rowid = $objp->rowid; // deprecated - $line->fk_facture = $objp->fk_facture; - $line->label = $objp->custom_label; // deprecated - $line->desc = $objp->description; // Description line - $line->description = $objp->description; // Description line - $line->product_type = $objp->product_type; // Type of line - $line->ref = $objp->product_ref; // Ref product - $line->product_ref = $objp->product_ref; // Ref product - $line->libelle = $objp->product_label; // deprecated + $line->fk_facture = $objp->fk_facture; + $line->label = $objp->custom_label; // deprecated + $line->desc = $objp->description; // Description line + $line->description = $objp->description; // Description line + $line->product_type = $objp->product_type; // Type of line + $line->ref = $objp->product_ref; // Ref product + $line->product_ref = $objp->product_ref; // Ref product + $line->libelle = $objp->product_label; // deprecated $line->product_label = $objp->product_label; // Label product $line->product_barcode = $objp->product_barcode; // Barcode number product - $line->product_desc = $objp->product_desc; // Description product + $line->product_desc = $objp->product_desc; // Description product $line->fk_product_type = $objp->fk_product_type; // Type of product - $line->qty = $objp->qty; - $line->subprice = $objp->subprice; - $line->ref_ext = $objp->ref_ext; // line external ref + $line->qty = $objp->qty; + $line->subprice = $objp->subprice; + $line->ref_ext = $objp->ref_ext; // line external ref $line->vat_src_code = $objp->vat_src_code; - $line->tva_tx = $objp->tva_tx; - $line->localtax1_tx = $objp->localtax1_tx; - $line->localtax2_tx = $objp->localtax2_tx; + $line->tva_tx = $objp->tva_tx; + $line->localtax1_tx = $objp->localtax1_tx; + $line->localtax2_tx = $objp->localtax2_tx; $line->localtax1_type = $objp->localtax1_type; $line->localtax2_type = $objp->localtax2_type; $line->remise_percent = $objp->remise_percent; $line->fk_remise_except = $objp->fk_remise_except; - $line->fk_product = $objp->fk_product; - $line->date_start = $this->db->jdate($objp->date_start); - $line->date_end = $this->db->jdate($objp->date_end); - $line->info_bits = $objp->info_bits; - $line->total_ht = $objp->total_ht; - $line->total_tva = $objp->total_tva; + $line->fk_product = $objp->fk_product; + $line->date_start = $this->db->jdate($objp->date_start); + $line->date_end = $this->db->jdate($objp->date_end); + $line->info_bits = $objp->info_bits; + $line->total_ht = $objp->total_ht; + $line->total_tva = $objp->total_tva; $line->total_localtax1 = $objp->total_localtax1; $line->total_localtax2 = $objp->total_localtax2; - $line->total_ttc = $objp->total_ttc; + $line->total_ttc = $objp->total_ttc; $line->code_ventilation = $objp->fk_code_ventilation; $line->fk_fournprice = $objp->fk_fournprice; @@ -2444,11 +2483,264 @@ public function fetchPreviousNextSituationInvoice() } /** - * Update database + * Compute the completed price of the invoice, ie. the total price if progress was 100% + * Exclusive of prorata, if any. + * Return the 2 digit rounded price. + * + * @return float + */ + public function computeCompletedPrice($rounded = true) + { + global $conf; + + // Define vars + $totalIfCompleted = 0; + + // Loop on all lines + foreach ($this->lines as $line) { + if (!class_exists('TSubtotal') || !TSubtotal::isModSubtotalLine($line) && $line->special_code != 10050172) { + // Compute the line price if progress is 100% + $totalIfCompleted += ($line->qty * $line->subprice) * (100 - $line->remise_percent) / 100 ; + } + } + + if ($rounded) { + return round($totalIfCompleted, 2); + } + + return $totalIfCompleted; + } + + /** + * Return all the FactureLine instances present in situation series + * + * @return array + */ + public function marginal_special_lines($outputlangs) { + // Gather previous situation data + $this->fetchPreviousNextSituationInvoice(); + $TPreviousInvoices = &$this->tab_previous_situation_invoice; + $prev_inv = end($TPreviousInvoices); + + // Temp var + $special_lines = array(); + + + // Go over invoice lines + foreach ($this->getSpecialLines() as $k => $line) { + $name = $outputlangs->convToOutputCharset($line->description); + $product_id = $line->fk_product; + $special_lines[$product_id] = array( + 'name' => $name, + 'amountHT' => $line->total_ht, + 'TVA' => (string) round($line->tva_tx, 1) . '%', + 'amountTTC' => $line->total_ttc, + ); + } + + if ($prev_inv) { + foreach ($prev_inv->getSpecialLines() as $k => $prev_line) { + $product_id = $prev_line->fk_product; + if (array_key_exists($product_id, $special_lines)) { + $special_lines[$product_id]['amountHT'] -= $prev_line->total_ht; + $special_lines[$product_id]['amountTTC'] -= $prev_line->total_ttc; + } + } + } + + return $special_lines; + } + + /** + * Compute the total HT of the invoice without the special-code lines + * Exclusive of prorata, if any. + * Return the 2 digit rounded price. + * + * @return float + */ + public function totalExeptSpecialLines() + { + global $conf; + + $total = 0; + + // Loop on all lines + foreach ($this->lines as $line) { + if (!class_exists('TSubtotal') || !TSubtotal::isModSubtotalLine($line) && $line->special_code != 10050172) { + $total += $line->total_ht ; + } + } + + return round($total, 2); + } + + /** + * Compute the VAT on the prorata, if any. + * Return the 2 digit rounded price. + * + * @return float + */ + public function tvaOnProrata() + { + global $conf; + + if ($this->total_ht != 0) { + $avg_vat_rate = $this->total_tva / $this->total_ht; + } else { + $avg_vat_rate = 0; + } + + return round($this->prorata_discount * $avg_vat_rate, 2); + } + + /** + * Provide the "BTP" TTC: TTC - prorata - VAT on prorata - retained warranty + * Return the 2 digit rounded price. + * + * @return float + */ + public function ttc_BTP() + { + global $conf; + return round($this->total_ttc - $this->prorata_discount - $this->tvaOnProrata() - $this->retained_warranty, 2); + } + + /** + * Return the special-code lines + * + * @return array of FactureLines + */ + public function getSpecialLines() + { + global $conf; + + $result = array(); + + // Loop on all lines + foreach ($this->lines as $line) { + if ($line->special_code == 10050172) { + $result[] = $line; + } + } + + return $result; + } + + /** + * Return extract of the special-code lines + * + * @return array of FactureLines + */ + public function getExtractSpecialLines($outputlangs) + { + global $conf; + + $result = array(); + + // Loop on all lines + foreach ($this->getSpecialLines() as $k => $line) { + $name = $outputlangs->convToOutputCharset($line->description); + $product_id = $line->fk_product; + if (array_key_exists($product_id, $result)) { + $result[$product_id]['amountHT'] = $result[$product_id]['amountHT'] + $line->total_ht; + $result[$product_id]['amountTTC'] = $result[$product_id]['amountTTC'] + $line->total_ttc; + } else { + $result[$product_id] = array( + 'name' => $name, + 'amountHT' => $line->total_ht, + 'TVA' => (string) round($line->tva_tx, 1) . '%', + 'amountTTC' => $line->total_ttc, + ); + } + } + + return $result; + } + + /** + * Return the completed price of the situation invoice' series, i.e. the total price if the last situation in the series was 100% + * Return the 2 digit rounded price. + * + * @return float + */ + public function getLastSituationCompletePrice($rounded = true) + { + global $conf; + + // Load situation invoices before and after this one + $this->fetchPreviousNextSituationInvoice(); + + // Get last invoice in the series + if (count($this->tab_next_situation_invoice) > 0) { + $last_in_serie = end($this->tab_next_situation_invoice); + } else { + $last_in_serie = $this; + } + + return $last_in_serie->computeCompletedPrice($rounded); + } + + /** + * Compute the global progress of the invoice. + * Return the 2 digit rounded progress, as percent. + * + * @return float + */ + public function computeGlobalProgress() + { + global $conf; + + // Define vars + $totalIfCompleted = $this->getLastSituationCompletePrice(); + $totalAvancement = 0; + $i=0; + + // Loop on all lines + foreach ($this->lines as $line) { + if (!class_exists('TSubtotal') || !TSubtotal::isModSubtotalLine($line) && $line->special_code != 10050172) { + // Compute the line price if progress is 100% + $totalAvancement += $line->total_ht; + } + } + + // Manage error + if (!empty($totalIfCompleted)) $avancementGlobal = $totalAvancement / $totalIfCompleted * 100; + else $avancementGlobal = 0; + + return round($avancementGlobal, 2); + } + + /** + * Compute the marginal progress of the invoice. + * Return the 2 digit rounded progress, as percent. + * + * @return float + */ + public function computeMarginalProgress() + { + global $conf; + + // Load situation invoices before and after this one + $this->fetchPreviousNextSituationInvoice(); + + // Get progress last invoice + $prev_progress = 0; + if (count($this->tab_previous_situation_invoice) > 0) { + $prev_invoice = end($this->tab_previous_situation_invoice); + $prev_progress = $prev_invoice->computeGlobalProgress(); + } + + // Compute marginal progress + return $this->computeGlobalProgress() - $prev_progress; + } + + + /** + * Update database * - * @param User $user User that modify - * @param int $notrigger 0=launch triggers after, 1=disable triggers - * @return int Return integer <0 if KO, >0 if OK + * @param User $user User that modify + * @param int $notrigger 0=launch triggers after, 1=disable triggers + * @return int Return integer <0 if KO, >0 if OK */ public function update(User $user, $notrigger = 0) { @@ -2498,6 +2790,15 @@ public function update(User $user, $notrigger = 0) $this->fk_user_author = $this->user_author; } + /* Manage prorata_dicsount in case of total_ht update + If rate and discount are inconsistent, rate overwrite (as a prorata) + -> TODO Remove with PHP8.4 set/get hooks on properties*/ + if (getDolGlobalString('INVOICE_USE_PRORATA_DISCOUNT') && isset($this->prorata_rate)) { + $discount_from_rate = $this->total_ht * $this->prorata_rate / 100; + if ($discount_from_rate != $this->prorata_discount) { + $this->prorata_discount = $discount_from_rate; + } + } // Check parameters // Put here code to add control on parameters values @@ -2541,7 +2842,8 @@ public function update(User $user, $notrigger = 0) $sql .= " situation_final=".(empty($this->situation_final) ? "0" : $this->db->escape($this->situation_final)).","; $sql .= " retained_warranty=".(empty($this->retained_warranty) ? "0" : $this->db->escape($this->retained_warranty)).","; $sql .= " retained_warranty_date_limit=".(strval($this->retained_warranty_date_limit) != '' ? "'".$this->db->idate($this->retained_warranty_date_limit)."'" : 'null').","; - $sql .= " retained_warranty_fk_cond_reglement=".(isset($this->retained_warranty_fk_cond_reglement) ? intval($this->retained_warranty_fk_cond_reglement) : "null"); + $sql .= " retained_warranty_fk_cond_reglement=".(isset($this->retained_warranty_fk_cond_reglement) ? intval($this->retained_warranty_fk_cond_reglement) : "null").","; + $sql .= " prorata_discount=".(isset($this->prorata_discount) ? $this->prorata_discount : "null"); $sql .= " WHERE rowid=".((int) $this->id); $this->db->begin(); @@ -2586,10 +2888,10 @@ public function update(User $user, $notrigger = 0) // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** - * Add a discount line into an invoice (as an invoice line) using an existing absolute discount (Consume the discount) + * Add a discount line into an invoice (as an invoice line) using an existing absolute discount (Consume the discount) * - * @param int $idremise Id of absolute discount - * @return int >0 if OK, <0 if KO + * @param int $idremise Id of absolute discount + * @return int >0 if OK, <0 if KO */ public function insert_discount($idremise) { @@ -2685,8 +2987,8 @@ public function insert_discount($idremise) /** * Set customer ref * - * @param string $ref_client Customer ref - * @param int $notrigger 1=Does not execute triggers, 0= execute triggers + * @param string $ref_client Customer ref + * @param int $notrigger 1=Does not execute triggers, 0= execute triggers * @return int Return integer <0 if KO, >0 if OK */ public function set_ref_client($ref_client, $notrigger = 0) @@ -2744,7 +3046,7 @@ public function set_ref_client($ref_client, $notrigger = 0) /** * Delete invoice * - * @param User $user User making the deletion. + * @param User $user User making the deletion. * @param int $notrigger 1=Does not execute triggers, 0= execute triggers * @param int $idwarehouse Id warehouse to use for stock change. * @return int Return integer <0 if KO, 0=Refused, >0 if OK @@ -2938,10 +3240,10 @@ public function delete($user, $notrigger = 0, $idwarehouse = -1) * * @deprecated * @see setPaid() - * @param User $user Object user that modify + * @param User $user Object user that modify * @param string $close_code Code renseigne si on classe a payee completement alors que paiement incomplet (cas escompte par exemple) * @param string $close_note Commentaire renseigne si on classe a payee alors que paiement incomplet (cas escompte par exemple) - * @return int Return integer <0 if KO, >0 if OK + * @return int Return integer <0 if KO, >0 if OK */ public function set_paid($user, $close_code = '', $close_note = '') { @@ -2955,10 +3257,10 @@ public function set_paid($user, $close_code = '', $close_note = '') * - paid completely (if close_code is not filled) => this->fk_statut=2, this->paye=1 * - or partially (if close_code filled) + appel trigger BILL_PAYED => this->fk_statut=2, this->paye stay 0 * - * @param User $user Object user that modify + * @param User $user Object user that modify * @param string $close_code Code renseigne si on classe a payee completement alors que paiement incomplet (cas escompte par exemple) * @param string $close_note Commentaire renseigne si on classe a payee alors que paiement incomplet (cas escompte par exemple) - * @return int Return integer <0 if KO, >0 if OK + * @return int Return integer <0 if KO, >0 if OK */ public function setPaid($user, $close_code = '', $close_note = '') { @@ -3020,8 +3322,8 @@ public function setPaid($user, $close_code = '', $close_note = '') * * @deprecated * @see setUnpaid() - * @param User $user Object user that change status - * @return int Return integer <0 if KO, >0 if OK + * @param User $user Object user that change status + * @return int Return integer <0 if KO, >0 if OK */ public function set_unpaid($user) { @@ -3035,8 +3337,8 @@ public function set_unpaid($user) * Fonction utilisee quand un paiement prelevement est refuse, * ou quand une facture annulee et reouverte. * - * @param User $user Object user that change status - * @return int Return integer <0 if KO, >0 if OK + * @param User $user Object user that change status + * @return int Return integer <0 if KO, >0 if OK */ public function setUnpaid($user) { @@ -3083,10 +3385,10 @@ public function setUnpaid($user) * * @deprecated * @see setCanceled() - * @param User $user Object user making change + * @param User $user Object user making change * @param string $close_code Code of closing invoice (CLOSECODE_REPLACED, CLOSECODE_...) * @param string $close_note Comment - * @return int Return integer <0 if KO, >0 if OK + * @return int Return integer <0 if KO, >0 if OK */ public function set_canceled($user, $close_code = '', $close_note = '') { @@ -3100,10 +3402,10 @@ public function set_canceled($user, $close_code = '', $close_note = '') * Warning, if option to decrease stock on invoice was set, this function does not change stock (it might be a cancel because * of no payment even if merchandises were sent). * - * @param User $user Object user making change + * @param User $user Object user making change * @param string $close_code Code of closing invoice (CLOSECODE_REPLACED, CLOSECODE_...) * @param string $close_note Comment - * @return int Return integer <0 if KO, >0 if OK + * @return int Return integer <0 if KO, >0 if OK */ public function setCanceled($user, $close_code = '', $close_note = '') { @@ -3160,7 +3462,7 @@ public function setCanceled($user, $close_code = '', $close_note = '') * Tag invoice as validated + call trigger BILL_VALIDATE * Object must have lines loaded with fetch_lines * - * @param User $user Object user that validate + * @param User $user Object user that validate * @param string $force_number Reference to force on invoice * @param int $idwarehouse Id of warehouse to use for stock decrease if option to decrease on stock is on (0=no decrease) * @param int $notrigger 1=Does not execute triggers, 0= execute triggers @@ -3750,23 +4052,23 @@ public function setDraft($user, $idwarehouse = -1) * par l'appelant par la methode get_default_tva(societe_vendeuse,societe_acheteuse,produit) * et le desc doit deja avoir la bonne valeur (a l'appelant de gerer le multilangue) * - * @param string $desc Description of line - * @param double $pu_ht Unit price without tax (> 0 even for credit note) - * @param double $qty Quantity - * @param double $txtva Force Vat rate, -1 for auto (Can contain the vat_src_code too with syntax '9.9 (CODE)') + * @param string $desc Description of line + * @param double $pu_ht Unit price without tax (> 0 even for credit note) + * @param double $qty Quantity + * @param double $txtva Force Vat rate, -1 for auto (Can contain the vat_src_code too with syntax '9.9 (CODE)') * @param double $txlocaltax1 Local tax 1 rate (deprecated, use instead txtva with code inside) * @param double $txlocaltax2 Local tax 2 rate (deprecated, use instead txtva with code inside) - * @param int $fk_product Id of predefined product/service - * @param double $remise_percent Percent of discount on line - * @param int|string $date_start Date start of service - * @param int|string $date_end Date end of service - * @param int $ventil Code of dispatching into accountancy - * @param int $info_bits Bits of type of lines - * @param int $fk_remise_except Id discount used + * @param int $fk_product Id of predefined product/service + * @param double $remise_percent Percent of discount on line + * @param int|string $date_start Date start of service + * @param int|string $date_end Date end of service + * @param int $ventil Code of dispatching into accountancy + * @param int $info_bits Bits of type of lines + * @param int $fk_remise_except Id discount used * @param string $price_base_type 'HT' or 'TTC' - * @param double $pu_ttc Unit price with tax (> 0 even for credit note) + * @param double $pu_ttc Unit price with tax (> 0 even for credit note) * @param int $type Type of line (0=product, 1=service). Not used if fk_product is defined, the type of product is used. - * @param int $rang Position of line (-1 means last value + 1) + * @param int $rang Position of line (-1 means last value + 1) * @param int $special_code Special code (also used by externals modules!) * @param string $origin Depend on global conf MAIN_CREATEFROM_KEEP_LINE_ORIGIN_INFORMATION can be 'orderdet', 'propaldet'..., else 'order','propal,'.... * @param int $origin_id Depend on global conf MAIN_CREATEFROM_KEEP_LINE_ORIGIN_INFORMATION can be Id of origin object (aka line id), else object id @@ -3775,13 +4077,13 @@ public function setDraft($user, $idwarehouse = -1) * @param int $pa_ht Buying price of line (to calculate margin) or '' * @param string $label Label of the line (deprecated, do not use) * @param array $array_options extrafields array - * @param int $situation_percent Situation advance percentage - * @param int $fk_prev_id Previous situation line id reference + * @param int $situation_percent Situation advance percentage + * @param int $fk_prev_id Previous situation line id reference * @param string $fk_unit Code of the unit to use. Null to use the default one * @param double $pu_ht_devise Unit price in foreign currency - * @param string $ref_ext External reference of the line + * @param string $ref_ext External reference of the line * @param int $noupdateafterinsertline No update after insert of line - * @return int Return integer <0 if KO, Id of line if OK + * @return int Return integer <0 if KO, Id of line if OK */ public function addline( $desc, @@ -4051,18 +4353,18 @@ public function addline( /** * Update a detail line * - * @param int $rowid Id of line to update - * @param string $desc Description of line - * @param double $pu Prix unitaire (HT ou TTC selon price_base_type) (> 0 even for credit note lines) - * @param double $qty Quantity - * @param double $remise_percent Percentage discount of the line - * @param int $date_start Date de debut de validite du service - * @param int $date_end Date de fin de validite du service - * @param double|string $txtva VAT Rate (Can be '8.5', '8.5 (ABC)') + * @param int $rowid Id of line to update + * @param string $desc Description of line + * @param double $pu Prix unitaire (HT ou TTC selon price_base_type) (> 0 even for credit note lines) + * @param double $qty Quantity + * @param double $remise_percent Percentage discount of the line + * @param int $date_start Date de debut de validite du service + * @param int $date_end Date de fin de validite du service + * @param double|string $txtva VAT Rate (Can be '8.5', '8.5 (ABC)') * @param double $txlocaltax1 Local tax 1 rate * @param double $txlocaltax2 Local tax 2 rate - * @param string $price_base_type HT or TTC - * @param int $info_bits Miscellaneous informations + * @param string $price_base_type HT or TTC + * @param int $info_bits Miscellaneous informations * @param int $type Type of line (0=product, 1=service) * @param int $fk_parent_line Id of parent line (0 in most cases, used by modules adding sublevels into lines). * @param int $skip_update_total Keep fields total_xxx to 0 (used for special lines by some modules) @@ -4071,13 +4373,13 @@ public function addline( * @param string $label Label of the line (deprecated, do not use) * @param int $special_code Special code (also used by externals modules!) * @param array $array_options extrafields array - * @param int $situation_percent Situation advance percentage + * @param int $situation_percent Situation advance percentage * @param string $fk_unit Code of the unit to use. Null to use the default one * @param double $pu_ht_devise Unit price in currency * @param int $notrigger disable line update trigger - * @param string $ref_ext External reference of the line - * @param integer $rang rank of line - * @return int Return integer < 0 if KO, > 0 if OK + * @param string $ref_ext External reference of the line + * @param integer $rang rank of line + * @return int Return integer < 0 if KO, > 0 if OK */ public function updateline($rowid, $desc, $pu, $qty, $remise_percent, $date_start, $date_end, $txtva, $txlocaltax1 = 0, $txlocaltax2 = 0, $price_base_type = 'HT', $info_bits = 0, $type = self::TYPE_STANDARD, $fk_parent_line = 0, $skip_update_total = 0, $fk_fournprice = null, $pa_ht = 0, $label = '', $special_code = 0, $array_options = array(), $situation_percent = 100, $fk_unit = null, $pu_ht_devise = 0, $notrigger = 0, $ref_ext = '', $rang = 0) { @@ -4321,9 +4623,9 @@ public function checkProgressLine($idline, $situation_percent) /** * Update invoice line with percentage * - * @param FactureLigne $line Invoice line - * @param int $percent Percentage - * @param boolean $update_price Update object price + * @param FactureLigne $line Invoice line + * @param int $percent Percentage + * @param boolean $update_price Update object price * @return void */ public function update_percent($line, $percent, $update_price = true) @@ -4424,9 +4726,9 @@ public function deleteline($rowid, $id = 0) * * @deprecated * @see setDiscount() - * @param User $user User that set discount - * @param double $remise Discount - * @param int $notrigger 1=Does not execute triggers, 0= execute triggers + * @param User $user User that set discount + * @param double $remise Discount + * @param int $notrigger 1=Does not execute triggers, 0= execute triggers * @return int Return integer <0 if KO, >0 if OK */ public function set_remise($user, $remise, $notrigger = 0) @@ -4439,9 +4741,9 @@ public function set_remise($user, $remise, $notrigger = 0) /** * Set percent discount * - * @param User $user User that set discount - * @param double $remise Discount - * @param int $notrigger 1=Does not execute triggers, 0= execute triggers + * @param User $user User that set discount + * @param double $remise Discount + * @param int $notrigger 1=Does not execute triggers, 0= execute triggers * @return int Return integer <0 if KO, >0 if OK * @deprecated remise_percent is a deprecated field for object parent */ @@ -4504,9 +4806,9 @@ public function setDiscount($user, $remise, $notrigger = 0) /** * Set absolute discount * - * @param User $user User that set discount - * @param double $remise Discount - * @param int $notrigger 1=Does not execute triggers, 0= execute triggers + * @param User $user User that set discount + * @param double $remise Discount + * @param int $notrigger 1=Does not execute triggers, 0= execute triggers * @return int Return integer <0 if KO, >0 if OK */ /* @@ -4569,12 +4871,12 @@ public function set_remise_absolue($user, $remise, $notrigger = 0) */ /** - * Return next reference of customer invoice not already used (or last reference) - * according to numbering module defined into constant FACTURE_ADDON + * Return next reference of customer invoice not already used (or last reference) + * according to numbering module defined into constant FACTURE_ADDON * - * @param Societe $soc object company - * @param string $mode 'next' for next value or 'last' for last value - * @return string free ref or last ref + * @param Societe $soc object company + * @param string $mode 'next' for next value or 'last' for last value + * @return string free ref or last ref */ public function getNextNumRef($soc, $mode = 'next') { @@ -4699,10 +5001,10 @@ public function info($id) $this->user_validation_id = $obj->fk_user_valid; $this->user_closing_id = $obj->fk_user_closing; - $this->date_creation = $this->db->jdate($obj->datec); + $this->date_creation = $this->db->jdate($obj->datec); $this->date_modification = $this->db->jdate($obj->datem); $this->date_validation = $this->db->jdate($obj->datev); - $this->date_closing = $this->db->jdate($obj->dateclosing); + $this->date_closing = $this->db->jdate($obj->dateclosing); } $this->db->free($result); } else { @@ -4716,14 +5018,14 @@ public function info($id) * Return list of invoices (eventually filtered on a user) into an array * * @param int $shortlist 0=Return array[id]=ref, 1=Return array[](id=>id,ref=>ref,name=>name) - * @param int $draft 0=not draft, 1=draft - * @param User $excluser Objet user to exclude - * @param int $socid Id third pary - * @param int $limit For pagination - * @param int $offset For pagination - * @param string $sortfield Sort criteria - * @param string $sortorder Sort order - * @return array|int -1 if KO, array with result if OK + * @param int $draft 0=not draft, 1=draft + * @param User $excluser Objet user to exclude + * @param int $socid Id third pary + * @param int $limit For pagination + * @param int $offset For pagination + * @param string $sortfield Sort criteria + * @param string $sortorder Sort order + * @return array|int -1 if KO, array with result if OK */ public function liste_array($shortlist = 0, $draft = 0, $excluser = null, $socid = 0, $limit = 0, $offset = 0, $sortfield = 'f.datef,f.rowid', $sortorder = 'DESC') { @@ -4793,7 +5095,7 @@ public function liste_array($shortlist = 0, $draft = 0, $excluser = null, $socid * (Status validated or abandonned for a reason 'other') + not payed + no payment at all + not already replaced * * @param int $socid Id thirdparty - * @return array|int Array of invoices ('id'=>id, 'ref'=>ref, 'status'=>status, 'paymentornot'=>0/1) + * @return array|int Array of invoices ('id'=>id, 'ref'=>ref, 'status'=>status, 'paymentornot'=>0/1) */ public function list_replacable_invoices($socid = 0) { @@ -4847,7 +5149,7 @@ public function list_replacable_invoices($socid = 0) * (validated + payment on process) or classified (payed completely or payed partiely) + not already replaced + not already a credit note * * @param int $socid Id thirdparty - * @return array|int Array of invoices ($id => array('ref'=>,'paymentornot'=>,'status'=>,'paye'=>) + * @return array|int Array of invoices ($id => array('ref'=>,'paymentornot'=>,'status'=>,'paye'=>) */ public function list_qualified_avoir_invoices($socid = 0) { @@ -4921,7 +5223,7 @@ public function list_qualified_avoir_invoices($socid = 0) /** * Load indicators for dashboard (this->nbtodo and this->nbtodolate) * - * @param User $user Object user + * @param User $user Object user * @return WorkboardResponse|int Return integer <0 if KO, WorkboardResponse if OK */ public function load_board($user) @@ -4988,7 +5290,7 @@ public function load_board($user) /** * Retourne id des contacts clients de facturation * - * @return array Liste des id contacts facturation + * @return array Liste des id contacts facturation */ public function getIdBillingContact() { @@ -4998,7 +5300,7 @@ public function getIdBillingContact() /** * Retourne id des contacts clients de livraison * - * @return array Liste des id contacts livraison + * @return array Liste des id contacts livraison */ public function getIdShippingContact() { @@ -5084,7 +5386,7 @@ public function initAsSpecimen($option = '') $line->localtax1_tx = 0; $line->localtax2_tx = 0; $line->remise_percent = 0; - if ($xnbp == 1) { // Qty is negative (product line) + if ($xnbp == 1) { // Qty is negative (product line) $prodid = mt_rand(1, $num_prods); $line->fk_product = $prodids[$prodid]; $line->qty = -1; @@ -5094,7 +5396,7 @@ public function initAsSpecimen($option = '') $line->multicurrency_total_ht = -200; $line->multicurrency_total_ttc = -239.2; $line->multicurrency_total_tva = -39.2; - } elseif ($xnbp == 2) { // UP is negative (free line) + } elseif ($xnbp == 2) { // UP is negative (free line) $line->subprice = -100; $line->total_ht = -100; $line->total_ttc = -119.6; @@ -5103,7 +5405,7 @@ public function initAsSpecimen($option = '') $line->multicurrency_total_ht = -200; $line->multicurrency_total_ttc = -239.2; $line->multicurrency_total_tva = -39.2; - } elseif ($xnbp == 3) { // Discount is 50% (product line) + } elseif ($xnbp == 3) { // Discount is 50% (product line) $prodid = mt_rand(1, $num_prods); $line->fk_product = $prodids[$prodid]; $line->total_ht = 50; @@ -5128,13 +5430,13 @@ public function initAsSpecimen($option = '') $this->lines[$xnbp] = $line; - $this->total_ht += $line->total_ht; - $this->total_tva += $line->total_tva; - $this->total_ttc += $line->total_ttc; + $this->total_ht += $line->total_ht; + $this->total_tva += $line->total_tva; + $this->total_ttc += $line->total_ttc; - $this->multicurrency_total_ht += $line->multicurrency_total_ht; - $this->multicurrency_total_tva += $line->multicurrency_total_tva; - $this->multicurrency_total_ttc += $line->multicurrency_total_ttc; + $this->multicurrency_total_ht += $line->multicurrency_total_ht; + $this->multicurrency_total_tva += $line->multicurrency_total_tva; + $this->multicurrency_total_ttc += $line->multicurrency_total_ttc; $xnbp++; } @@ -5165,9 +5467,9 @@ public function initAsSpecimen($option = '') // phpcs:disable PEAR.NamingConventions.ValidFunctionName.ScopeNotCamelCaps /** - * Load indicators for dashboard (this->nbtodo and this->nbtodolate) + * Load indicators for dashboard (this->nbtodo and this->nbtodolate) * - * @return int Return integer <0 if KO, >0 if OK + * @return int Return integer <0 if KO, >0 if OK */ public function load_state_board() { @@ -5217,11 +5519,11 @@ public function getLinesArray() * * @param string $modele Generator to use. Caller must set it to obj->model_pdf or GETPOST('model','alpha') for example. * @param Translate $outputlangs Object lang to use for translation - * @param int $hidedetails Hide details of lines - * @param int $hidedesc Hide description - * @param int $hideref Hide ref - * @param null|array $moreparams Array to provide more information - * @return int Return integer <0 if KO, >0 if OK + * @param int $hidedetails Hide details of lines + * @param int $hidedesc Hide description + * @param int $hideref Hide ref + * @param null|array $moreparams Array to provide more information + * @return int Return integer <0 if KO, >0 if OK */ public function generateDocument($modele, $outputlangs, $hidedetails = 0, $hidedesc = 0, $hideref = 0, $moreparams = null) { @@ -5322,8 +5624,8 @@ public function get_prev_sits() /** * Sets the invoice as a final situation * - * @param User $user Object user - * @param int $notrigger 1=Does not execute triggers, 0= execute triggers + * @param User $user Object user + * @param int $notrigger 1=Does not execute triggers, 0= execute triggers * @return int Return integer <0 if KO, >0 if OK */ public function setFinal(User $user, $notrigger = 0) @@ -5636,7 +5938,7 @@ public function setRetainedWarrantyDateLimit($timestamp, $dateYmd = '') * @param int|string $template Name (or id) of email template (Must be a template of type 'facture_send') * @param string $datetouse 'duedate' (default) or 'invoicedate' * @param string $forcerecipient Force email of recipient (for example to send the email to an accountant supervisor instead of the customer) - * @return int 0 if OK, <>0 if KO (this function is used also by cron so only 0 is OK) + * @return int 0 if OK, <>0 if KO (this function is used also by cron so only 0 is OK) */ public function sendEmailsRemindersOnInvoiceDueDate($nbdays = 0, $paymentmode = 'all', $template = '', $datetouse = 'duedate', $forcerecipient = '') { @@ -5985,7 +6287,7 @@ public function willBeLastOfSameType($allow_validated_drafts = false) /** * Return clicable link of object (with eventually picto) * - * @param string $option Where point the link (0=> main card, 1,2 => shipment, 'nolink'=>No link) + * @param string $option Where point the link (0=> main card, 1,2 => shipment, 'nolink'=>No link) * @param array $arraydata Array of data * @return string HTML Code for Kanban thumb. */ @@ -6025,6 +6327,27 @@ public function getKanbanView($option = '', $arraydata = null) $return .= ''; return $return; } + + /** + * Set prorata_discount from rate + * To change with PHP8.4 + * + * @param float $rate Prorata rate, between 0 and 100 + * @return string 1 if OK, -1 if NOK. + */ + public function setProrataFromRate($rate = 0) + { + global $langs, $user; + + try { + $this->prorata_discount = (float)$rate / 100 * $this->total_ht; + $this->prorata_rate = (float)$rate; + $res = $this->update($user); + } catch (Exception $e) { + return -1; + } + return $res; + } } /** @@ -6119,9 +6442,9 @@ class FactureLigne extends CommonInvoiceLine public $fk_prev_id; /** - * Constructor + * Constructor * - * @param DoliDB $db handler d'acces base de donnee + * @param DoliDB $db handler d'acces base de donnee */ public function __construct($db) { @@ -6131,7 +6454,7 @@ public function __construct($db) /** * Load invoice line from database * - * @param int $rowid id of invoice line to get + * @param int $rowid id of invoice line to get * @return int Return integer <0 if KO, >0 if OK */ public function fetch($rowid) @@ -6206,8 +6529,8 @@ public function fetch($rowid) $this->fk_user_modif = $objp->fk_user_modif; $this->fk_user_author = $objp->fk_user_author; - $this->situation_percent = $objp->situation_percent; - $this->fk_prev_id = $objp->fk_prev_id; + $this->situation_percent = $objp->situation_percent; + $this->fk_prev_id = $objp->fk_prev_id; $this->multicurrency_subprice = $objp->multicurrency_subprice; $this->multicurrency_total_ht = $objp->multicurrency_total_ht; @@ -6228,9 +6551,9 @@ public function fetch($rowid) /** * Insert line into database * - * @param int $notrigger 1 no triggers - * @param int $noerrorifdiscountalreadylinked 1=Do not make error if lines is linked to a discount and discount already linked to another - * @return int Return integer <0 if KO, >0 if OK + * @param int $notrigger 1 no triggers + * @param int $noerrorifdiscountalreadylinked 1=Do not make error if lines is linked to a discount and discount already linked to another + * @return int Return integer <0 if KO, >0 if OK */ public function insert($notrigger = 0, $noerrorifdiscountalreadylinked = 0) { @@ -6655,9 +6978,9 @@ public function update($user = null, $notrigger = 0) /** * Delete line in database * - * @param User $tmpuser User that deletes + * @param User $tmpuser User that deletes * @param bool $notrigger false=launch triggers after, true=disable triggers - * @return int Return integer <0 if KO, >0 if OK + * @return int Return integer <0 if KO, >0 if OK */ public function delete($tmpuser = null, $notrigger = false) { @@ -6767,9 +7090,9 @@ public function update_total() * Returns situation_percent of the previous line. * Warning: If invoice is a replacement invoice, this->fk_prev_id is id of the replaced line. * - * @param int $invoiceid Invoice id - * @param bool $include_credit_note Include credit note or not - * @return int >= 0 + * @param int $invoiceid Invoice id + * @param bool $include_credit_note Include credit note or not + * @return int >= 0 */ public function get_prev_progress($invoiceid, $include_credit_note = true) { diff --git a/htdocs/core/actions_setnotes.inc.php b/htdocs/core/actions_setnotes.inc.php index b247c1c2cc5c3..8fcd7ed60109a 100644 --- a/htdocs/core/actions_setnotes.inc.php +++ b/htdocs/core/actions_setnotes.inc.php @@ -23,12 +23,11 @@ // $action must be defined -// $permissionnote must be defined to permission to edit object // $object must be defined (object is loaded in this file with fetch) // $id must be defined (object is loaded in this file with fetch) // Set public note -if ($action == 'setnote_public' && !empty($permissionnote) && !GETPOST('cancel', 'alpha')) { +if ($action == 'setnote_public' && !GETPOST('cancel', 'alpha')) { if (empty($action) || !is_object($object) || empty($id)) { dol_print_error('', 'Include of actions_setnotes.inc.php was done but required variable was not set before'); } @@ -72,7 +71,7 @@ } } } -} elseif ($action == 'setnote_private' && !empty($permissionnote) && !GETPOST('cancel', 'alpha')) { // Set public note +} elseif ($action == 'setnote_private' && !GETPOST('cancel', 'alpha')) { // Set public note if (empty($user->socid)) { // Private notes (always hidden to external users) if (empty($action) || !is_object($object) || empty($id)) { diff --git a/htdocs/core/class/commonobject.class.php b/htdocs/core/class/commonobject.class.php index 91969f39dc99a..f24606f175215 100644 --- a/htdocs/core/class/commonobject.class.php +++ b/htdocs/core/class/commonobject.class.php @@ -3874,6 +3874,16 @@ public function update_price($exclspec = 0, $roundingadjust = 'none', $nodatabas $this->db->free($resql); + /* Manage prorata_discount in case of total_ht update + If rate and discount are inconsistent, rate overwrite (as a prorata) + -> TODO Remove with PHP8.4 set/get hooks on properties*/ + if (getDolGlobalString('INVOICE_USE_PRORATA_DISCOUNT') && isset($this->prorata_rate)) { + $discount_from_rate = $this->total_ht * $this->prorata_rate / 100; + if ($discount_from_rate != $this->prorata_discount) { + $this->prorata_discount = $discount_from_rate; + } + } + // Now update global fields total_ht, total_ttc, total_tva, total_localtax1, total_localtax2, multicurrency_total_* of main object $fieldht = 'total_ht'; $fieldtva = 'tva'; @@ -3895,6 +3905,9 @@ public function update_price($exclspec = 0, $roundingadjust = 'none', $nodatabas $sql .= ", multicurrency_total_ht = ".((float) price2num($this->multicurrency_total_ht, 'MT', 1)); $sql .= ", multicurrency_total_tva = ".((float) price2num($this->multicurrency_total_tva, 'MT', 1)); $sql .= ", multicurrency_total_ttc = ".((float) price2num($this->multicurrency_total_ttc, 'MT', 1)); + if (isset($this->prorata_discount)) { + $sql .= ", prorata_discount = ". ((float) $this->prorata_discount); + } $sql .= " WHERE rowid = ".((int) $this->id); dol_syslog(get_class($this)."::update_price", LOG_DEBUG); diff --git a/htdocs/core/class/html.form.class.php b/htdocs/core/class/html.form.class.php index 8a285587b79d2..0deda2522b6f7 100644 --- a/htdocs/core/class/html.form.class.php +++ b/htdocs/core/class/html.form.class.php @@ -4786,7 +4786,7 @@ public function formSelectShippingMethod($page, $selected = '', $htmlname = 'shi * * @return string HTML select */ - public function selectSituationInvoices($selected = '', $socid = 0) + public function selectSituationInvoices($selected = '', $socid = 0, $project = 0) { global $langs; @@ -4799,6 +4799,9 @@ public function selectSituationInvoices($selected = '', $socid = 0) $sql .= ' AND situation_counter >= 1'; $sql .= ' AND fk_soc = ' . (int) $socid; $sql .= ' AND type <> 2'; + if ($project > 0) { + $sql .= ' AND fk_projet = ' . (int) $project; + } $sql .= ' ORDER by situation_cycle_ref, situation_counter desc'; $resql = $this->db->query($sql); @@ -9249,10 +9252,11 @@ public function showLinkedObjectBlock($object, $morehtmlright = '', $compatibleI * @param array $excludelinksto Do not show links of this type, for exemple array('order') or array('supplier_order'). null or array() if no exclusion. * @return string HTML block */ - public function showLinkToObjectBlock($object, $restrictlinksto = array(), $excludelinksto = array()) + public function showLinkToObjectBlock($object, $restrictlinksto = array(), $excludelinksto = array(), $same_project_filter = false) { global $conf, $langs, $hookmanager; global $action; + global $same_project_filter; $linktoelem = ''; $linktoelemlist = ''; @@ -9278,75 +9282,89 @@ public function showLinkToObjectBlock($object, $restrictlinksto = array(), $excl unset($tmpproject); } + // Enable a per project filter + function project_filter($label, $fk) { + $project_filter = ''; + global $same_project_filter; + if (($fk > 0) && getDolGlobalString('FILTER_ON_SAME_PROJECT_IN_LINKTO') && (isset($same_project_filter) && $same_project_filter)) { + $filter = 'fk_projet'; + if ($label == 'mo' || $label == 'ticket') { + $filter = 'fk_project'; + } + $project_filter = 'AND t.'.$filter.' = ' . $fk; + } + return $project_filter; + } + $possiblelinks = array( 'propal' => array( 'enabled' => isModEnabled('propal'), 'perms' => 1, 'label' => 'LinkToProposal', - 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.ref_client, t.total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "propal as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('propal') . ')'), + 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.ref_client, t.total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "propal as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('propal') . ')' . project_filter('propal', $object->fk_project) ), 'shipping' => array( 'enabled' => isModEnabled('expedition'), 'perms' => 1, 'label' => 'LinkToExpedition', - 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "expedition as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('shipping') . ')'), + 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "expedition as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('shipping') . ')'. project_filter('shipping', $object->fk_project) ), 'order' => array( 'enabled' => isModEnabled('commande'), 'perms' => 1, 'label' => 'LinkToOrder', - 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.ref_client, t.total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "commande as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('commande') . ')'), + 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.ref_client, t.total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "commande as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('commande') . ')'. project_filter('order', $object->fk_project) ), 'invoice' => array( 'enabled' => isModEnabled('facture'), 'perms' => 1, 'label' => 'LinkToInvoice', - 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.ref_client, t.total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "facture as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('invoice') . ')'), + 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.ref_client, t.total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "facture as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('invoice') . ')'. project_filter('invoice', $object->fk_project) ), 'invoice_template' => array( 'enabled' => isModEnabled('facture'), 'perms' => 1, 'label' => 'LinkToTemplateInvoice', - 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.titre as ref, t.total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "facture_rec as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('invoice') . ')'), + 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.titre as ref, t.total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "facture_rec as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('invoice') . ')'. project_filter('invoice_template', $object->fk_project) ), 'contrat' => array( 'enabled' => isModEnabled('contrat'), 'perms' => 1, 'label' => 'LinkToContract', 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.ref_customer as ref_client, t.ref_supplier, SUM(td.total_ht) as total_ht - FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "contrat as t, " . $this->db->prefix() . "contratdet as td WHERE t.fk_soc = s.rowid AND td.fk_contrat = t.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('contract') . ') GROUP BY s.rowid, s.nom, s.client, t.rowid, t.ref, t.ref_customer, t.ref_supplier' + FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "contrat as t, " . $this->db->prefix() . "contratdet as td WHERE t.fk_soc = s.rowid AND td.fk_contrat = t.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('contract') . ')'. project_filter('contrat', $object->fk_project) . ' GROUP BY s.rowid, s.nom, s.client, t.rowid, t.ref, t.ref_customer, t.ref_supplier' ), 'fichinter' => array( 'enabled' => isModEnabled('ficheinter'), 'perms' => 1, 'label' => 'LinkToIntervention', - 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "fichinter as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('intervention') . ')'), + 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "fichinter as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('intervention') . ')' . project_filter('intervention', $object->fk_project) ), 'supplier_proposal' => array( 'enabled' => isModEnabled('supplier_proposal'), 'perms' => 1, 'label' => 'LinkToSupplierProposal', - 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, '' as ref_supplier, t.total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "supplier_proposal as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('supplier_proposal') . ')'), + 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, '' as ref_supplier, t.total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "supplier_proposal as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('supplier_proposal') . ')'. project_filter('supplier_proposal', $object->fk_project) ), 'order_supplier' => array( 'enabled' => isModEnabled("supplier_order"), 'perms' => 1, 'label' => 'LinkToSupplierOrder', - 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.ref_supplier, t.total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "commande_fournisseur as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('commande_fournisseur') . ')'), + 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.ref_supplier, t.total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "commande_fournisseur as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('commande_fournisseur') . ')'. project_filter('order_supplier', $object->fk_project) ), 'invoice_supplier' => array( 'enabled' => isModEnabled("supplier_invoice"), 'perms' => 1, 'label' => 'LinkToSupplierInvoice', - 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.ref_supplier, t.total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "facture_fourn as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('facture_fourn') . ')'), + 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.ref_supplier, t.total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "facture_fourn as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('facture_fourn') . ')'. project_filter('invoice_supplier', $object->fk_project) ), 'ticket' => array( 'enabled' => isModEnabled('ticket'), 'perms' => 1, 'label' => 'LinkToTicket', - 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.track_id, '0' as total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "ticket as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('ticket') . ')'), + 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.track_id, '0' as total_ht FROM " . $this->db->prefix() . "societe as s, " . $this->db->prefix() . "ticket as t WHERE t.fk_soc = s.rowid AND t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('ticket') . ')'. project_filter('ticket', $object->fk_project) ), 'mo' => array( 'enabled' => isModEnabled('mrp'), 'perms' => 1, 'label' => 'LinkToMo', - 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.rowid, '0' as total_ht FROM " . $this->db->prefix() . "societe as s INNER JOIN " . $this->db->prefix() . "mrp_mo as t ON t.fk_soc = s.rowid WHERE t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('mo') . ')') + 'sql' => "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.rowid, '0' as total_ht FROM " . $this->db->prefix() . "societe as s INNER JOIN " . $this->db->prefix() . "mrp_mo as t ON t.fk_soc = s.rowid WHERE t.fk_soc IN (" . $this->db->sanitize($listofidcompanytoscan) . ') AND t.entity IN (' . getEntity('mo') . ')'. project_filter('mo', $object->fk_project) ) ); } if ($object->table_element == 'commande_fournisseur') { - $possiblelinks['mo']['sql'] = "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.rowid, '0' as total_ht FROM ".$this->db->prefix()."societe as s INNER JOIN ".$this->db->prefix().'mrp_mo as t ON t.fk_soc = s.rowid WHERE t.entity IN ('.getEntity('mo').')'; + $possiblelinks['mo']['sql'] = "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.rowid, '0' as total_ht FROM ".$this->db->prefix()."societe as s INNER JOIN ".$this->db->prefix().'mrp_mo as t ON t.fk_soc = s.rowid WHERE t.entity IN ('.getEntity('mo').')' . project_filter('mo', $object->fk_project) ; } elseif ($object->table_element == 'mrp_mo') { - $possiblelinks['order_supplier']['sql'] = "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.ref_supplier, t.total_ht FROM ".$this->db->prefix()."societe as s, ".$this->db->prefix().'commande_fournisseur as t WHERE t.fk_soc = s.rowid AND t.entity IN ('.getEntity('commande_fournisseur').')'; + $possiblelinks['order_supplier']['sql'] = "SELECT s.rowid as socid, s.nom as name, s.client, t.rowid, t.ref, t.ref_supplier, t.total_ht FROM ".$this->db->prefix()."societe as s, ".$this->db->prefix().'commande_fournisseur as t WHERE t.fk_soc = s.rowid AND t.entity IN ('.getEntity('commande_fournisseur').')'. project_filter('invoice_supplier', $object->fk_project) ; } if (!empty($listofidcompanytoscan)) { // If empty, we don't have criteria to scan the object we can link to @@ -9465,8 +9483,14 @@ public function showLinkToObjectBlock($object, $restrictlinksto = array(), $excl } if ($linktoelemlist) { - $linktoelem = ' - - + @@ -462,7 +462,7 @@ '; } - if (!empty($inputalsopricewithtax) && !getDolGlobalInt('MAIN_NO_INPUT_PRICE_WITH_TAX')) { + if (showInputTtc($object, $inputalsopricewithtax)) { $coldisplay++; $upinctax = isset($line->pu_ttc) ? $line->pu_ttc : null; if (getDolGlobalInt('MAIN_UNIT_PRICE_WITH_TAX_IS_FOR_ALL_TAXES')) { diff --git a/htdocs/core/tpl/objectline_title.tpl.php b/htdocs/core/tpl/objectline_title.tpl.php index 8e16d09492db7..ec5b65ea3a83d 100644 --- a/htdocs/core/tpl/objectline_title.tpl.php +++ b/htdocs/core/tpl/objectline_title.tpl.php @@ -43,7 +43,7 @@ print "\n"; // Title line -print "\n"; +print ''; print ''; @@ -92,7 +92,7 @@ print ''; } -if (!empty($inputalsopricewithtax) && !getDolGlobalInt('MAIN_NO_INPUT_PRICE_WITH_TAX')) { +if (!empty($inputalsopricewithtax) && showInputTtc($object, $inputalsopricewithtax)) { print ''; } diff --git a/htdocs/core/tpl/objectline_view.tpl.php b/htdocs/core/tpl/objectline_view.tpl.php index 8f98f555d04b3..dd0e1d5048d18 100644 --- a/htdocs/core/tpl/objectline_view.tpl.php +++ b/htdocs/core/tpl/objectline_view.tpl.php @@ -332,7 +332,7 @@ +if (showInputTtc($object, $inputalsopricewithtax)) { ?>
trans('PriceUHTCurrency'); ?> trans('PriceUTTC'); ?> trans('Qty'); ?> "> @@ -1263,7 +1263,11 @@ function setforpredef() { /* jQuery("#tva_tx, #title_vat").hide(); */ /* jQuery("#title_fourn_ref").hide(); */ - jQuery("#np_marginRate, #np_markRate, .np_marginRate, .np_markRate, #units, #title_units").hide(); + + jQuery("#np_marginRate, #np_markRate, .np_marginRate, .np_markRate").hide(); + + jQuery("#np_marginRate, #np_markRate, .np_marginRate, .np_markRate, #units, #title_units").hide(); + jQuery("#buying_price").show(); jQuery('#trlinefordates, .divlinefordates').show(); } diff --git a/htdocs/core/tpl/objectline_edit.tpl.php b/htdocs/core/tpl/objectline_edit.tpl.php index 8c88628bef6c9..053e0b70a4b24 100644 --- a/htdocs/core/tpl/objectline_edit.tpl.php +++ b/htdocs/core/tpl/objectline_edit.tpl.php @@ -225,7 +225,7 @@ print '
'.$langs->trans('PriceUHTCurrency', $this->multicurrency_code).''.$langs->trans('PriceUTTC').'multicurrency_subprice); ?> pu_ttc) ? $line->pu_ttc : null; if (getDolGlobalInt('MAIN_UNIT_PRICE_WITH_TAX_IS_FOR_ALL_TAXES')) { diff --git a/htdocs/core/triggers/interface_99_modFacture_CouffignalFactureSituation.class.php b/htdocs/core/triggers/interface_99_modFacture_CouffignalFactureSituation.class.php new file mode 100644 index 0000000000000..dc9576e91b903 --- /dev/null +++ b/htdocs/core/triggers/interface_99_modFacture_CouffignalFactureSituation.class.php @@ -0,0 +1,208 @@ +. + */ + +/** + * \file core/triggers/interface_99_modFacture_CouffignalFactureSituation.class.php + * \ingroup facture + * \brief Couffignal facture situation. + */ + +require_once DOL_DOCUMENT_ROOT.'/core/triggers/dolibarrtriggers.class.php'; +require_once DOL_DOCUMENT_ROOT.'/couffignal/FactureTools.php'; + + +/** + * Class of triggers for MyModule module + */ +class InterfaceCouffignalFactureSituation extends DolibarrTriggers +{ + /** + * Constructor + * + * @param DoliDB $db Database handler + */ + public function __construct($db) + { + $this->db = $db; + + $this->name = preg_replace('/^Interface/i', '', get_class($this)); + $this->family = "facture"; + $this->description = "Couffignal facture situation."; + // 'development', 'experimental', 'dolibarr' or version + $this->version = 'development'; + $this->picto = 'couffignal_facture_situation'; + } + + /** + * 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 Return integer <0 if KO, 0 if no triggered ran, >0 if OK + */ + public function runTrigger($action, $object, User $user, Translate $langs, Conf $conf) + { + if (!isModEnabled('facture') || !isModEnabled('project')) { + return 0; + } + + if (($object instanceof Facture) && ((int) $object->type === Facture::TYPE_SITUATION)) { + if ($action === 'BILL_VALIDATE') { + $difference = round(FactureTools::calculateDifference($this->db, $object), 2); + + if (abs($difference) >= 0.01) { + setEventMessage($langs->trans('InvoiceSituationMustHaveOrdersImportedForBeingValidated', number_format($difference, 2)), 'warnings'); + } + return 1; + } + } + + // SpecialCode devis client + if (($object instanceof PropaleLigne)) { + if ($action === 'LINEPROPAL_INSERT') { + $productSpecialCode = $this->getSpecialCodeFromProductId($object->fk_product); + if ($productSpecialCode === 0) { + return 0; + } + $object->special_code = $productSpecialCode; + $object->update(true); + return 1; + } + } + + // SpecialCode devis fournisseur + if (($object instanceof SupplierProposalLine)) { + if ($action === 'LINESUPPLIER_PROPOSAL_INSERT') { + $productSpecialCode = $this->getSpecialCodeFromProductId($object->fk_product); + if ($productSpecialCode === 0) { + return 0; + } + $object->special_code = $productSpecialCode; + $object->update(true); + return 1; + } + } + + // SpecialCode commande client + if (($object instanceof OrderLine)) { + if ($action === 'LINEORDER_INSERT') { + $productSpecialCode = $this->getSpecialCodeFromProductId($object->fk_product); + if ($productSpecialCode === 0) { + return 0; + } + $object->special_code = $productSpecialCode; + $object->update($user, true); + return 1; + } + } + + // SpecialCode commande fournisseur + if (($object instanceof CommandeFournisseurLigne)) { + if ($action === 'LINEORDER_SUPPLIER_CREATE') { + $productSpecialCode = $this->getSpecialCodeFromProductId($object->fk_product); + if ($productSpecialCode === 0) { + return 0; + } + $object->special_code = $productSpecialCode; + $object->update(true); + return 1; + } + } + + // SpecialCode facture client + if (($object instanceof FactureLigne)) { + if ($action === 'LINEBILL_INSERT') { + $productSpecialCode = $this->getSpecialCodeFromProductId($object->fk_product); + if ($productSpecialCode === 0) { + return 0; + } + + $object->special_code = $productSpecialCode; + $object->update($user, true); + return 1; + } + } + + // SpecialCode facture fournisseur + if (($object instanceof SupplierInvoiceLine)) { + if ($action === 'LINEBILL_SUPPLIER_CREATE') { + $productSpecialCode = $this->getSpecialCodeFromProductId($object->fk_product); + if ($productSpecialCode === 0) { + return 0; + } + $object->special_code = $productSpecialCode; + + /** + * SupplierInvoiceLine::update() does not update the special_code :-/ + */ + $sql = 'UPDATE '.MAIN_DB_PREFIX.'facture_fourn_det t'; + $sql .= ' SET t.special_code = '.$this->db->escape($object->special_code); + $sql .= ' WHERE t.rowid = '.(int) $object->id; + $this->db->query($sql); + if ($this->db->error()) { + setEventMessage($this->db->lasterror(), 'errors'); + return -1; + } + + return 1; + } + } + + return 0; + } + + /** + * Get special code from product id. + * + * @param int $productId ID of the product + * @return int + */ + private function getSpecialCodeFromProductId($productId): int + { + $product = new Product($this->db); + $product->fetch($productId); + if (empty($product->array_options) || !array_key_exists('options_specialcodeauto', $product->array_options)) { + return 0; + } + return (int) $product->array_options['options_specialcodeauto']; + } +} diff --git a/htdocs/couffignal/CoSousTraitant.php b/htdocs/couffignal/CoSousTraitant.php new file mode 100644 index 0000000000000..62e9f83cffd72 --- /dev/null +++ b/htdocs/couffignal/CoSousTraitant.php @@ -0,0 +1,171 @@ +fetchPreviousNextSituationInvoice(); + $cumulated_invoices_of_cycle = array_merge([$facture], $facture->tab_previous_situation_invoice); + $all_invoices_of_cycle = array_merge($cumulated_invoices_of_cycle, $facture->tab_next_situation_invoice); + + // -- Part 1 - Fetch related Orders to and Invoices from suppliers + $supplier_docs = array( + 'direct' => self::fetchSousTraitantsCoTraitantsData($db, $facture, 'unique'), + 'cumulated' => self::fetchSousTraitantsCoTraitantsData($db, $facture, 'cumulated'), + 'all' => self::fetchSousTraitantsCoTraitantsData($db, $facture, 'full_cycle'), + ); + + // Manage case where all docs are empty, not to show the table + $zero_docs = True; + foreach ($supplier_docs['all'] as $key => $doc_array) { + if (!empty($doc_array)) { + $zero_docs = False; + break; + } + } + if ($zero_docs) { + return []; + } + + // -- Part 2 - Fetch related Customer Orders + $orders_related_to_cycle = []; + foreach ($all_invoices_of_cycle as $inv_in_cycle) { + $orders_related = FactureTools::getTotalHtOrdersLinkedToInvoice($db, $inv_in_cycle); + $orders_related_to_cycle = array_merge($orders_related_to_cycle, $orders_related); + } + $orders_related_to_cycle = array_map("unserialize", array_unique(array_map("serialize", $orders_related_to_cycle))); + + // -- Part 3 - Calculate values and results + $sum_tot_ht_of = fn($arr) => empty($arr) ? 0 : array_sum(array_column($arr, 'total_ht')); + $sum_tot_ttc_of = fn($arr) => empty($arr) ? 0 : array_sum(array_column($arr, 'total_ttc')); + $sum_prorata_ht_of = fn($arr) => empty($arr) ? 0 : array_sum(array_column($arr, 'prorata_discount')); + + // Total facturé Couffignal : Factures Clients cumulées - factures Co-trait cumulée + $sumCumFacturedCoTrait = $sum_tot_ht_of($supplier_docs['cumulated']['invoices_co_trait']); + $cumulatedTotalHTMainCompany = $sum_tot_ht_of($cumulated_invoices_of_cycle) - $sum_prorata_ht_of($cumulated_invoices_of_cycle); + $facturedMainCompany = $cumulatedTotalHTMainCompany - $sumCumFacturedCoTrait; + + // Total à payer Couffignal + $sumDirFacturedCoTrait = $sum_tot_ttc_of($supplier_docs['direct']['invoices_co_trait']); + $sumDirFactureSsTrait = $sum_tot_ht_of($supplier_docs['direct']['invoices_ss_trait']); + $ToPayMainCompany = $facture->ttc_BTP() - $sumDirFacturedCoTrait - $sumDirFactureSsTrait; + // Note: Supplier invoices related data are cumulated up to now (column invoiced), while orders related data are for all the serie (column Marché) + + return [ + 'company' => [ + 'name' => 'Couffignal', + 'market' => ['sum_total_ht' => $sum_tot_ht_of($orders_related_to_cycle)], + 'factured' => ['sum_total_ht' => round($facturedMainCompany, 2)], + 'to_pay' => ['sum_total_ttc' => round($ToPayMainCompany, 2)] + ], + 'co_trait' => self::structureFacturesBySocid($db, $supplier_docs['all']['orders_co_trait'], $supplier_docs['cumulated']['invoices_co_trait'], $supplier_docs['direct']['invoices_co_trait']), + 'sous_trait' => self::structureFacturesBySocid($db, $supplier_docs['all']['orders_ss_trait'], $supplier_docs['cumulated']['invoices_ss_trait'], $supplier_docs['direct']['invoices_ss_trait']), + ]; + } + + + /** + * Charge les factures fournisseurs et commandes des sous-traitants et co-traitants à partir d'une facture. + * + * @param DoliDB $db Gestionnaire de base de données. + * @param Facture $facture Facture à traiter. + * @param string $mode Mode:'unique', cumulated', 'full_cycle' for all documents attached to resp. this invoice, invoices in cycle up to the given one, all invoices in cycle. + * @return array of array + */ + public static function fetchSousTraitantsCoTraitantsData(DoliDB $db, Facture $facture, string $mode): array + { + // Fetch related Orders and Invoices + $facturesFourn = FactureFournisseurTools::getFacturesFournValidatedFromDebtCompensationLinks($facture, $db, $mode); + $commandesFourn = FactureFournisseurTools::getOrdersValidatedFromFacturesFourn($facturesFourn); + + // CommandeFournisseur via extrafields: options_typefournisseur, 1 pour co-traitant, 2 pour sous-traitant + $filterCommandesFourn = fn($arr, $type) => array_filter($arr, fn(CommandeFournisseur $el) => + array_key_exists('options_typefournisseur', $el->array_options) && $el->array_options['options_typefournisseur'] == $type + ); + + $result = array( + 'orders_co_trait' => $filterCommandesFourn($commandesFourn, '1'), + 'orders_ss_trait' => $filterCommandesFourn($commandesFourn, '2'), + // Autoliquidation de TVA = sous traitant, pas d'autoliquidation de TVA = co-traitant + 'invoices_co_trait' => array_filter($facturesFourn, fn($el) => !$el->vat_reverse_charge), + 'invoices_ss_trait' => array_filter($facturesFourn, fn($el) => $el->vat_reverse_charge), + ); + + foreach ($result as $i => $docs) { + if (empty($docs)) {$result[$i] = [];} + } + + return $result; + } + + + /** + * Structure les commandes et factures fournisseurs par société. + * + * @param DoliDB $db Gestionnaire de base de données. + * @param array $allCommandesFourn Liste des commandes fournisseurs sur toute la série de factures client. + * @param array $CumulatedFacturesFourn Liste des factures fournisseurs cumulées sur la série de factures client jusqu'à la facture actuelle. + * @param array $DirectFacturesFourn Liste des factures fournisseurs directement liées à la facture actuelle. + * @return array + */ + private static function structureFacturesBySocid(DoliDB $db, array $allCommandesFourn, array $CumulatedFacturesFourn, array $DirectFacturesFourn): array + { + $result = []; + + // Initialisation des données par société + foreach (array_merge($allCommandesFourn, $CumulatedFacturesFourn) as $el) { + if (!isset($result[$el->socid])) { + $fournisseur = new Fournisseur($db); + $fournisseur->fetch($el->socid); + $result[$el->socid] = [ + 'soc_id' => $el->socid, + 'name' => $fournisseur->name, + 'market' => ['sum_total_ht' => 0, 'sum_total_ttc' => 0, 'summary' => []], + 'factured' => ['sum_total_ht' => 0, 'sum_total_ttc' => 0, 'summary' => []], + 'to_pay' => ['sum_total_ht' => 0, 'sum_total_ttc' => 0, 'summary' => []], + ]; + } + } + + $params = array( + 'market' => $allCommandesFourn, + 'factured' => $CumulatedFacturesFourn, + 'to_pay' => $DirectFacturesFourn, + ); + + foreach ($params as $key => $docs) { + foreach ($docs as $d) { + $result[$d->socid][$key]['sum_total_ht'] += $d->total_ht; + $result[$d->socid][$key]['sum_total_ttc'] += $d->total_ttc; + $result[$d->socid][$key]['summary'][$d->id] = [ + 'id' => $d->id, + 'ref' => $d->ref, + 'total_ht' => $d->total_ht, + ]; + } + } + + // Tri des commandes et factures par référence + foreach ($result as &$soc) { + foreach ($params as $key => $docs) { + usort($soc[$key]['summary'], fn($a, $b) => strcmp($a['ref'], $b['ref'])); + } + } + + return $result; + } +} diff --git a/htdocs/couffignal/CommandeTools.php b/htdocs/couffignal/CommandeTools.php new file mode 100644 index 0000000000000..792cb5b2257c8 --- /dev/null +++ b/htdocs/couffignal/CommandeTools.php @@ -0,0 +1,34 @@ +date <=> $b->date; + if ($dateComparison === 0) { + return $a->ref <=> $b->ref; + } + return $dateComparison; + }); + + return $orders; + } +} diff --git a/htdocs/couffignal/FactureFournisseurTools.php b/htdocs/couffignal/FactureFournisseurTools.php new file mode 100644 index 0000000000000..7430703187ae5 --- /dev/null +++ b/htdocs/couffignal/FactureFournisseurTools.php @@ -0,0 +1,94 @@ +getLinkedObjects($invoice, $db); + + // Safety filter on Validated and Closed supplier invoices only + $facturesFourn = array(); + foreach ($factureFournLinked as $factureFourn) { + $statusValid = [FactureFournisseur::STATUS_VALIDATED, FactureFournisseur::STATUS_CLOSED]; + if (in_array($factureFourn->status, $statusValid)) { + $facturesFourn[$factureFourn->ref] = $factureFourn; + } + } + + // Recursive calls managing the calls over invoicing cycle + switch ($cycle) { + case 'unique': + break; + case 'cumulated': + $invoice->fetchPreviousNextSituationInvoice(); + foreach ($invoice->tab_previous_situation_invoice as $inv) { + $facturesFourn = array_merge($facturesFourn, self::getFacturesFournValidatedFromDebtCompensationLinks($inv, $db, 'unique')); + } + break; + case 'full_cycle': + $invoice->fetchPreviousNextSituationInvoice(); + foreach (array_merge($invoice->tab_previous_situation_invoice, $invoice->tab_next_situation_invoice) as $inv) { + $facturesFourn = array_merge($facturesFourn, self::getFacturesFournValidatedFromDebtCompensationLinks($inv, $db, 'unique')); + } + break; + } + + return $facturesFourn; + } + + /** + * Récupère les commandes fournisseurs validées à partir des factures fournisseurs liées à la facture client. + * + * @param Project $project Projet lié à la facture. + * @return CommandeFournisseur[] Tableau d'objets de commandes fournisseurs. + */ + public static function getOrdersValidatedFromFacturesFourn(array $invoices): array + { + + $orders = []; + foreach ($invoices as $ref => $invoice) { + $invoice->fetchObjectLinked(); + + if (empty($invoice->linkedObjects['order_supplier'])) { continue; } + foreach ($invoice->linkedObjects['order_supplier'] as $id => $order) { + if (in_array($order->statut, [ + CommandeFournisseur::STATUS_VALIDATED, + CommandeFournisseur::STATUS_ACCEPTED, + CommandeFournisseur::STATUS_ORDERSENT, + CommandeFournisseur::STATUS_RECEIVED_PARTIALLY, + CommandeFournisseur::STATUS_RECEIVED_COMPLETELY + ])) { + $orders[$order->ref] = $order; + } + } + } + + $orders = CommandeTools::sortOrdersByDateAndRef($orders); + + return $orders; + } +} diff --git a/htdocs/couffignal/FactureTools.php b/htdocs/couffignal/FactureTools.php new file mode 100644 index 0000000000000..a367788e388d0 --- /dev/null +++ b/htdocs/couffignal/FactureTools.php @@ -0,0 +1,59 @@ +fetchObjectLinked(); + + $orders = []; + if (empty($invoice->linkedObjects['commande'])) { return []; } + foreach ($invoice->linkedObjects['commande'] as $id => $order) { + if (in_array($order->statut, [ + Commande::STATUS_VALIDATED, + Commande::STATUS_SHIPMENTONPROCESS, + Commande::STATUS_ACCEPTED + ])) { + $orders[] = $order; + } + } + $orders = CommandeTools::sortOrdersByDateAndRef($orders); + if ($load_obj) { + return array_map(static fn ($o) => ['ref_client' => $o->ref_ext . '(' .$o->ref.')', 'total_ht' => $o->total_ht, 'obj' => $o], $orders); + } + return array_map(static fn ($o) => ['ref_client' => $o->ref_ext . '(' .$o->ref.')', 'total_ht' => $o->total_ht], $orders); + } + + /** + * Calculate the difference between the last situation complete price and the total HT of orders linked to the project of the invoice. + * + * @param DoliDB $db Database handler + * @param Facture $facture Invoice object + * @return float The difference + */ + public static function calculateDifference(DoliDB $db, Facture $facture): float + { + $lastSituationCompletePrice = $facture->getLastSituationCompletePrice(false); + $totalHtOrders = self::getTotalHtOrdersLinkedToInvoice($db, $facture); + $sumTotalHtOrders = array_sum(array_column($totalHtOrders, 'total_ht')); + + return $lastSituationCompletePrice - $sumTotalHtOrders; + } +} diff --git a/htdocs/couffignal/ProjectNodeView.php b/htdocs/couffignal/ProjectNodeView.php new file mode 100644 index 0000000000000..dcaf88aa701cd --- /dev/null +++ b/htdocs/couffignal/ProjectNodeView.php @@ -0,0 +1,225 @@ + [ + 'custom_ref_attr' => 'ref_client', + 'color' => '#65953d', + 'link' => '/commande/card.php?id=' + ], + 'Facture' => [ + 'custom_ref_attr' => 'ref_client', + 'color' => '#a23121', + 'link' => '/compta/facture/card.php?facid=' + ], + 'CommandeFournisseur' => [ + 'custom_ref_attr' => 'ref_supplier', + 'color' => '#599caf', + 'link' => '/fourn/commande/card.php?id=' + ], + 'FactureFournisseur' => [ + 'custom_ref_attr' => 'ref_supplier', + 'color' => '#6059af', + 'link' => '/fourn/facture/card.php?facid=', + ] + ]; + + /** + * Get the seeds - the starting point of the invoicing cycles - present in the project + * + * @param DoliDB $db + * @param Project $project Project object for which we search seeds + * @param array $properties Properties for get_element_list + * @param string $dates Forwarded + * @param string $datee Forwarded + * + * @return array List of Invoices (Facture) + */ + public static function getCycleSeeds(DoliDB $db, Project $project, array $properties, string $dates, string $datee): array + { + // Collect all invoices + $list_invoices = $project->get_element_list('invoice', $properties['table'], $properties['datefieldname'], $dates, $datee, 'fk_projet'); + + // Assume only one serie of invoices + $seeds = []; + foreach ($list_invoices as $k => $id) { + $inv = new Facture($db); + $inv->fetch($id); + if ($inv->is_first()) { + // To support + $inv->fetchPreviousNextSituationInvoice(); + $seeds[] = $inv; + } + } + + return $seeds; + } + + + /** + * Load a Node from its representing object + * + * @param Object Facture|FactureFournisseur|Commande|CommandeFournisseur $obj + * + * @return array Array representing a node + */ + public static function loadNode(Facture|FactureFournisseur|Commande|CommandeFournisseur $obj): array + { + if (array_key_exists(get_class($obj), self::MANAGED_ELEMENTS)) { + $k = get_class($obj); + $custom_ref_attr = self::MANAGED_ELEMENTS[$k]['custom_ref_attr']; + return [ + 'id' => $obj->ref, + 'label' => $obj->ref . ' - ' . $obj->$custom_ref_attr, + 'obj' => $obj, + 'type' => $k, + 'link' => self::MANAGED_ELEMENTS[$k]['link'].$obj->id, + 'tooltip' => $obj->getNomUrl(1), + ]; + } + return []; + } + + + /** + * Load Invoices nodes related to an invoicing cycle + * + * @param array $seed List of nodes representing the FactureFournisseur related to invoicing cycle + * @param array $links Links object for the graph + * * + * @return array List of Invoices nodes (Facture) + */ + public static function loadInvoicesNodes(Facture $seed, array &$links): array + { + // Initialize invoice nodes with invoices + $inv_nodes = []; + foreach (array_merge([$seed], $seed->tab_next_situation_invoice) as $inv) { + $inv_nodes[$inv->ref] = self::loadNode($inv); + } + + // Initialize links between nodes + $keys = array_keys($inv_nodes); + foreach ($keys as $index => $key) { + // Check if there is a next key to avoid out-of-bounds + if (isset($keys[$index + 1])) { + $links[] = [ + 'from' => $inv_nodes[$key]['id'], + 'to' => $inv_nodes[$keys[$index + 1]]['id'] + ]; + } + } + + return $inv_nodes; + } + + + /** + * Load Orders nodes for all invoice nodes of a cycle + * + * @param array $inv_nodes List of nodes representing the Factures in invoicing cycle + * @param array $links Links object for the graph + * + * @return array List of Orders nodes (Commande) + */ + public static function loadOrdersNodes(DoliDB $db, array $inv_nodes, array &$links): array + { + $order_nodes = []; + foreach ($inv_nodes as $i => $node) { + $orders = FactureTools::getTotalHtOrdersLinkedToInvoice($db, $node['obj'], True); + // Add new nodes + foreach ($orders as $o) { + $order_nodes[$o['obj']->ref] = self::loadNode($o['obj']); + // Create links + $o['obj']->fetchObjectLinked(); + foreach ($o['obj']->linkedObjects['facture'] as $id => $facture) { + $links[] = [ + 'from' => $o['obj']->ref, + 'to' => $facture->ref + ]; + } + } + } + + return $order_nodes; + } + + + /** + * Load Supplier Invoices nodes for all invoice nodes of a cycle + * + * @param array $inv_nodes List of nodes representing the Factures in invoicing cycle + * @param array $links Links object for the graph + * + * @return array List of Supplier Invoices nodes (FactureFournisseur) + */ + public static function loadSupplierInvoicesNodes(DoliDB $db, array $inv_nodes, array &$links): array + { + $su_inv_nodes = []; + foreach ($inv_nodes as $i => $node) { + $obj = new LinkClientPayFourn($db); + $su_invs = $obj->getLinkedObjects($node['obj'], $db); + + // Add new nodes + foreach ($su_invs as $su_i) { + $su_inv_nodes[$su_i->ref] = self::loadNode($su_i); + // Create links + $links[] = [ + 'from' => $node['id'], + 'to' => $su_i->ref + ]; + } + } + + return $su_inv_nodes; + } + + + /** + * Load Supplier Orders nodes for all supplier invoice nodes related to an invoicing cycle + * + * @param array $su_inv_nodes List of nodes representing the FactureFournisseur related to invoicing cycle + * @param array $links Links object for the graph + * + * @return array List of Supplier Orders nodes (Commande) + */ + public static function loadSupplierOrdersNodes(array $su_inv_nodes, array &$links): array + { + $su_order_nodes = []; + foreach ($su_inv_nodes as $i => $node) { + $su_orders = FactureFournisseurTools::getOrdersValidatedFromFacturesFourn([$node['obj']]); + // Add new nodes + foreach ($su_orders as $o) { + $su_order_nodes[$o->ref] = self::loadNode($o); + // Create links + $o->fetchObjectLinked(); + foreach ($o->linkedObjects['invoice_supplier'] as $id => $su_inv) { + $links[] = [ + 'from' => $o->ref, + 'to' => $su_inv->ref + ]; + } + } + } + + return $su_order_nodes; + } + +} \ No newline at end of file diff --git a/htdocs/couffignal/tests/CommandeToolsTest.php b/htdocs/couffignal/tests/CommandeToolsTest.php new file mode 100644 index 0000000000000..859b08e564ec6 --- /dev/null +++ b/htdocs/couffignal/tests/CommandeToolsTest.php @@ -0,0 +1,75 @@ +date = 10; + $obj2 = new stdClass(); + $obj2->date = 20; + $list = [$obj2, $obj]; + $sorted = CommandeTools::sortOrdersByDateAndRef($list); + $this->assertEquals($obj, $sorted[0]); + $this->assertEquals($obj2, $sorted[1]); + } + + /** + * It should sort orders by reference. + * + * @return void + */ + public function testItShouldSortByRef(): void + { + $obj1 = new stdClass(); // 2 + $obj1->date = 10; + $obj1->ref = 'A'; + $obj2 = new stdClass(); // 3 + $obj2->date = 10; + $obj2->ref = 'B'; + $obj3 = new stdClass(); // 1 + $obj3->date = 4; + $obj3->ref = 'C'; + $obj4 = new stdClass(); // 4 + $obj4->date = 20; + $obj4->ref = 'D'; + $list = [$obj2, $obj4, $obj1, $obj3]; + $sorted = CommandeTools::sortOrdersByDateAndRef($list); + $this->assertEquals($obj3, $sorted[0]); + $this->assertEquals($obj1, $sorted[1]); + $this->assertEquals($obj2, $sorted[2]); + $this->assertEquals($obj4, $sorted[3]); + } + + /** + * It should return an empty array when no orders are provided. + * + * @return void + */ + public function testItShouldReturnEmptyArray(): void + { + $list = []; + $sorted = CommandeTools::sortOrdersByDateAndRef($list); + $this->assertEquals([], $sorted); + } +} diff --git a/htdocs/expensereport/card.php b/htdocs/expensereport/card.php index 613017d859887..d471e0d0c8953 100644 --- a/htdocs/expensereport/card.php +++ b/htdocs/expensereport/card.php @@ -964,7 +964,7 @@ if ($action == "confirm_setdraft" && GETPOST('confirm', 'alpha') == "yes" && $id > 0 && $user->hasRight('expensereport', 'creer')) { $object = new ExpenseReport($db); $object->fetch($id); - if ($user->id == $object->fk_user_author || $user->id == $object->fk_user_valid) { + if ( ($user->id == $object->fk_user_author || $user->id == $object->fk_user_valid) || (getDolGlobalInt('MAIN_GIVE_EXPENSEAPPROVER_RIGTHTOMODIFY') && ($user->hasRight('expensereport', 'approve'))) ) { $result = $object->setStatut(0); if ($result > 0) { diff --git a/htdocs/fourn/commande/card.php b/htdocs/fourn/commande/card.php index 74fea9a945be7..45d7de5fd171b 100644 --- a/htdocs/fourn/commande/card.php +++ b/htdocs/fourn/commande/card.php @@ -88,6 +88,7 @@ $origin = GETPOST('origin', 'alpha'); $originid = (GETPOST('originid', 'int') ? GETPOST('originid', 'int') : GETPOST('origin_id', 'int')); // For backward compatibility $rank = (GETPOST('rank', 'int') > 0) ? GETPOST('rank', 'int') : -1; +$same_project_filter = GETPOST('sameproject', 'string') == "on"; // PDF $hidedetails = (GETPOST('hidedetails', 'int') ? GETPOST('hidedetails', 'int') : (getDolGlobalString('MAIN_GENERATE_DOCUMENTS_HIDE_DETAILS') ? 1 : 0)); @@ -156,7 +157,6 @@ } // Permissions for includes -$permissionnote = $usercancreate; // Used by the include of actions_setnotes.inc.php $permissiondellink = $usercancreate; // Used by the include of actions_dellink.inc.php $permissiontoedit = $usercancreate; // Used by the include of actions_lineupdown.inc.php $permissiontoadd = $usercancreate; // Used by the include of actions_addupdatedelete.inc.php @@ -602,7 +602,7 @@ if (empty($pu)) { $pu = 0; // If pu is '' or null, we force to have a numeric value } - + $result = $object->addline( $desc, ($price_base_type == 'HT' ? $pu : 0), @@ -622,7 +622,7 @@ $date_start, $date_end, $array_options, - $productsupplier->fk_unit, + (getDolGlobalInt('MAIN_EDIT_LINE_SET_UNIT_ON_EXISTING_PRODUCT') ? GETPOST('units', 'alpha') : $productsupplier->fk_unit), $pu_devise, '', 0, @@ -1926,7 +1926,7 @@ $title = $langs->trans('ProductsAndServices'); print load_fiche_titre($title); - print '
'; + print '
'; print ''; $selectedLines = array(); @@ -2482,7 +2482,7 @@ include DOL_DOCUMENT_ROOT.'/core/tpl/ajaxrow.tpl.php'; } - print '
'; + print '
'; print '
'; // Add free products/services form @@ -2783,7 +2783,7 @@ $somethingshown = $formfile->numoffiles; // Show links to link elements - $linktoelem = $form->showLinkToObjectBlock($object, null, array('supplier_order', 'order_supplier')); + $linktoelem = $form->showLinkToObjectBlock($object, null, array('supplier_order', 'order_supplier'), $same_project_filter); $somethingshown = $form->showLinkedObjectBlock($object, $linktoelem); print '
'; diff --git a/htdocs/fourn/commande/note.php b/htdocs/fourn/commande/note.php index cacddcd4865a3..33f2d01980c29 100644 --- a/htdocs/fourn/commande/note.php +++ b/htdocs/fourn/commande/note.php @@ -55,7 +55,6 @@ $object->fetch($id, $ref); // Permissions -$permissionnote = ($user->hasRight("fournisseur", "commande", "creer") || $user->hasRight("supplier_order", "creer")); // Used by the include of actions_setnotes.inc.php $usercancreate = ($user->hasRight("fournisseur", "commande", "creer") || $user->hasRight("supplier_order", "creer")); $permissiontoadd = $usercancreate; // Used by the include of actions_addupdatedelete.inc.php diff --git a/htdocs/fourn/facture/card-rec.php b/htdocs/fourn/facture/card-rec.php index 4275abe24f325..8cea82d049f51 100644 --- a/htdocs/fourn/facture/card-rec.php +++ b/htdocs/fourn/facture/card-rec.php @@ -1086,7 +1086,7 @@ /* * Invoice lines */ - print '
'; + print '
'; print '
'; // Show object lines if (!empty($object->lines)) { @@ -1567,7 +1567,7 @@ include DOL_DOCUMENT_ROOT . '/core/tpl/ajaxrow.tpl.php'; } - print '
'; + print '
'; print '
'; $object->fetch_lines(); // Show object lines diff --git a/htdocs/fourn/facture/card.php b/htdocs/fourn/facture/card.php index 6791444e8b28f..2dcab90b85e49 100644 --- a/htdocs/fourn/facture/card.php +++ b/htdocs/fourn/facture/card.php @@ -84,6 +84,7 @@ $originid = GETPOST('originid', 'int'); $fac_recid = GETPOST('fac_rec', 'int'); $rank = (GETPOST('rank', 'int') > 0) ? GETPOST('rank', 'int') : -1; +$same_project_filter = GETPOST('sameproject', 'string') == "on"; // PDF $hidedetails = (GETPOST('hidedetails', 'int') ? GETPOST('hidedetails', 'int') : (getDolGlobalString('MAIN_GENERATE_DOCUMENTS_HIDE_DETAILS') ? 1 : 0)); @@ -131,7 +132,6 @@ $usercansend = (!getDolGlobalString('MAIN_USE_ADVANCED_PERMS') || $user->hasRight("fournisseur", "supplier_invoice_advance", "send")); // Permissions for includes -$permissionnote = $usercancreate; // Used by the include of actions_setnotes.inc.php $permissiondellink = $usercancreate; // Used by the include of actions_dellink.inc.php $permissiontoedit = $usercancreate; // Used by the include of actions_lineupdown.inc.php $permissiontoadd = $usercancreate; // Used by the include of actions_addupdatedelete.inc.php and actions_lineupdown.inc.php @@ -1743,7 +1743,7 @@ min($rank, count($object->lines) + 1), 0, $array_options, - $productsupplier->fk_unit, + (getDolGlobalInt('MAIN_EDIT_LINE_SET_UNIT_ON_EXISTING_PRODUCT') ? GETPOST('units', 'alpha') : $productsupplier->fk_unit), 0, $pu_devise, GETPOST('fourn_ref', 'alpha'), @@ -2851,7 +2851,7 @@ function setRadioForTypeOfInvoice() { $title = $langs->trans('ProductsAndServices'); print load_fiche_titre($title); - print '
'; + print '
'; print '
'; $objectsrc->printOriginLinesList('', $selectedLines); @@ -3645,7 +3645,7 @@ function setRadioForTypeOfInvoice() { $num = $db->num_rows($result); $i = 0; - print '
'; + print '
'; print '
'; print ''; print ''; @@ -3948,7 +3948,7 @@ function setRadioForTypeOfInvoice() { include DOL_DOCUMENT_ROOT.'/core/tpl/ajaxrow.tpl.php'; } - print '
'; + print '
'; print '
'.($object->type == FactureFournisseur::TYPE_CREDIT_NOTE ? $langs->trans("PaymentsBack") : $langs->trans('Payments')).'
'; global $forceall, $senderissupplier, $dateSelector, $inputalsopricewithtax; @@ -4180,7 +4180,7 @@ function setRadioForTypeOfInvoice() { $somethingshown = $formfile->numoffiles; // Show links to link elements - $linktoelem = $form->showLinkToObjectBlock($object, null, array('invoice_supplier')); + $linktoelem = $form->showLinkToObjectBlock($object, null, array('invoice_supplier'), $same_project_filter); $somethingshown = $form->showLinkedObjectBlock($object, $linktoelem); print '
'; diff --git a/htdocs/fourn/facture/note.php b/htdocs/fourn/facture/note.php index e66f1efb45bb2..8e29a2707de0a 100644 --- a/htdocs/fourn/facture/note.php +++ b/htdocs/fourn/facture/note.php @@ -55,8 +55,6 @@ $usercancreate = ($user->hasRight("fournisseur", "facture", "creer") || $user->hasRight("supplier_invoice", "creer")); $permissiontoadd = $usercancreate; -$permissionnote = ($user->hasRight("fournisseur", "facture", "creer") || $user->hasRight("supplier_invoice", "creer")); // Used by the include of actions_setnotes.inc.php - /* * Actions diff --git a/htdocs/langs/en_US/accountancy.lang b/htdocs/langs/en_US/accountancy.lang index 418c3eb86b62c..afca30fb1761b 100644 --- a/htdocs/langs/en_US/accountancy.lang +++ b/htdocs/langs/en_US/accountancy.lang @@ -57,12 +57,14 @@ WarningDataDisappearsWhenDataIsExported=Warning, this list contains only the acc VueByAccountAccounting=View by accounting account VueBySubAccountAccounting=View by accounting subaccount +ProtataDiscountAccountancy=Prorata charges MainAccountForCustomersNotDefined=Main account (from the Chart of Account) for customers not defined in setup MainAccountForSuppliersNotDefined=Main account (from the Chart of Account) for vendors not defined in setup MainAccountForUsersNotDefined=Main account (from the Chart of Account) for users not defined in setup MainAccountForVatPaymentNotDefined=Account (from the Chart of Account) for VAT payment not defined in setup MainAccountForSubscriptionPaymentNotDefined=Account (from the Chart of Account) for membership payment not defined in setup MainAccountForRetainedWarrantyNotDefined=Account (from the Chart of Account) for the retained warranty not defined in setup +MainAccountForProrataNotDefined=Account (from the Chart of Account) for the prorata charges not defined in setup UserAccountNotDefined=Accounting account for user not defined in setup AccountancyArea=Accounting area @@ -201,6 +203,7 @@ UseAuxiliaryAccountOnCustomerDeposit=Store customer account as individual accoun ACCOUNTING_ACCOUNT_SUPPLIER_DEPOSIT=Account (from the Chart Of Account) to be used as the default account to register supplier deposit UseAuxiliaryAccountOnSupplierDeposit=Store supplier account as individual account in subsidiary ledger for lines of down payments (if disabled, individual account for down payment lines will remain empty) ACCOUNTING_ACCOUNT_CUSTOMER_RETAINED_WARRANTY=Accounting account by default to register customer retained warranty +ACCOUNTING_ACCOUNT_CUSTOMER_PRORATA_DISCOUNT=Accounting account by default to register prorata charges ACCOUNTING_PRODUCT_BUY_ACCOUNT=Account (from the Chart Of Account) to be used as the default account for the products purchased within same country (used if not defined in the product sheet) ACCOUNTING_PRODUCT_BUY_INTRA_ACCOUNT=Account (from the Chart Of Account) to be used as the default account for the products purchased from EEC to another EEC country (used if not defined in the product sheet) diff --git a/htdocs/langs/en_US/bills.lang b/htdocs/langs/en_US/bills.lang index 6820a501716a5..aff7e99b79f84 100644 --- a/htdocs/langs/en_US/bills.lang +++ b/htdocs/langs/en_US/bills.lang @@ -233,6 +233,7 @@ AllowedInvoiceForRetainedWarranty=Retained warranty usable on the following type RetainedwarrantyDefaultPercent=Retained warranty default percent RetainedwarrantyOnlyForSituation=Make "retained warranty" available only for situation invoices RetainedwarrantyOnlyForSituationFinal=On situation invoices the global "retained warranty" deduction is applied only on the final situation +InvoiceSituationMustHaveOrdersImportedForBeingValidated=Not all validated orders are in the latest available status invoice, calculate difference: %s HT ToPayOn=To pay on %s toPayOn=to pay on %s RetainedWarranty=Retained Warranty diff --git a/htdocs/langs/en_US/compta.lang b/htdocs/langs/en_US/compta.lang index d40d89d1d960d..7d56f43821a25 100644 --- a/htdocs/langs/en_US/compta.lang +++ b/htdocs/langs/en_US/compta.lang @@ -316,3 +316,7 @@ InvoiceToPay30Days=To pay (> 30 days) ConfirmPreselectAccount=Preselect accountancy code ConfirmPreselectAccountQuestion=Are you sure you want to preselect the %s selected lines with this accountancy code ? AmountPaidMustMatchAmountOfDownPayment=Amount paid must match amount of down payment +SituationSerieTotal=Total amount contract +ProrataRate=Rate of prorata retain +SetProrataRate=Set rate of prorata retain +ProrataRetain=Prorata retain \ No newline at end of file diff --git a/htdocs/langs/fr_FR/accountancy.lang b/htdocs/langs/fr_FR/accountancy.lang index c5b1a476e0762..3266b71e05388 100644 --- a/htdocs/langs/fr_FR/accountancy.lang +++ b/htdocs/langs/fr_FR/accountancy.lang @@ -57,12 +57,15 @@ WarningDataDisappearsWhenDataIsExported=Attention, cette liste ne contient que l VueByAccountAccounting=Vue par comptes comptables VueBySubAccountAccounting=Affichage par compte auxiliaire +ProtataDiscountAccountancy=Compte prorata + MainAccountForCustomersNotDefined=Compte comptable général pour les clients non défini dans la configuration MainAccountForSuppliersNotDefined=Compte comptable général pour les fournisseurs non défini dans la configuration MainAccountForUsersNotDefined=Compte comptable général pour les utilisateurs non défini dans la configuration MainAccountForVatPaymentNotDefined=Compte comptable général pour les paiements de TVA non défini dans la configuration MainAccountForSubscriptionPaymentNotDefined=Compte comptable général pour les paiements d'adhésion non défini dans la configuration MainAccountForRetainedWarrantyNotDefined=Compte comptable par défaut pour la retenue de garantie non défini dans le paramétrage +MainAccountForProrataNotDefined=Compte comptable par défaut pour le compte prorata non défini dans le paramétrage UserAccountNotDefined=Compte comptable pour l'utilisateur non défini dans la configuration AccountancyArea=Espace comptabilité @@ -201,6 +204,7 @@ UseAuxiliaryAccountOnCustomerDeposit=Enregistrer le compte client comme compte i ACCOUNTING_ACCOUNT_SUPPLIER_DEPOSIT=Compte comptable par défaut pour enregistrer l'acompte fournisseur UseAuxiliaryAccountOnSupplierDeposit=Utiliser le compte comptable auxiliaire fournisseur dans le grand livre auxiliaire pour les lignes d'acomptes fournisseurs (si désactivé, le compte comptable auxiliaire pour les lignes d'acompte restera vide) ACCOUNTING_ACCOUNT_CUSTOMER_RETAINED_WARRANTY=Compte comptable par défaut pour l'enregistrement des retenues de garantie clients +ACCOUNTING_ACCOUNT_CUSTOMER_PRORATA_DISCOUNT=Compte comptable par défaut pour le compte prorata déduit sur facturation client ACCOUNTING_PRODUCT_BUY_ACCOUNT=Compte comptable par défaut pour les produits achetés dans le même pays (utilisé si non défini dans la fiche produit) ACCOUNTING_PRODUCT_BUY_INTRA_ACCOUNT=Compte comptable par défaut pour les produits achetés dans la CEE (utilisé si non défini dans la fiche produit) diff --git a/htdocs/langs/fr_FR/bills.lang b/htdocs/langs/fr_FR/bills.lang index de5ce64620e3f..d6a83073bcf92 100644 --- a/htdocs/langs/fr_FR/bills.lang +++ b/htdocs/langs/fr_FR/bills.lang @@ -233,6 +233,7 @@ AllowedInvoiceForRetainedWarranty=Garantie conservée utilisable sur les types d RetainedwarrantyDefaultPercent=Pourcentage par défaut de la retenue de garantie RetainedwarrantyOnlyForSituation=Rendre la "retenue de garantie" disponible uniquement pour les factures de situation RetainedwarrantyOnlyForSituationFinal=Sur les factures de situation, la déduction globale pour "retenue de garantie" n'est appliquée que sur la facture de situation finale +InvoiceSituationMustHaveOrdersImportedForBeingValidated=Toutes les commandes validées ne sont pas dans la dernière facture de situation disponible, différence calculée: %s HT ToPayOn=À payer le %s toPayOn=à payer le %s RetainedWarranty=Retenue de garantie diff --git a/htdocs/langs/fr_FR/compta.lang b/htdocs/langs/fr_FR/compta.lang index fa80457a31793..50d102b509b8f 100644 --- a/htdocs/langs/fr_FR/compta.lang +++ b/htdocs/langs/fr_FR/compta.lang @@ -313,3 +313,7 @@ InvoiceToPay30Days=A payer (dans plus de 30 jours) ConfirmPreselectAccount=Présélectionner le code comptable ConfirmPreselectAccountQuestion=Êtes-vous sûr de vouloir présélectionner les %s lignes sélectionnées avec ce code comptable ? AmountPaidMustMatchAmountOfDownPayment=Le montant payé doit correspondre au montant de l'acompte +ProrataRate=Taux prorata +SetProrataRate=Changer le taux de prorata +ProrataRetain=Retenue prorata +CompensatedDebt=Compensation de créance \ No newline at end of file diff --git a/htdocs/projet/element.php b/htdocs/projet/element.php index 09b5642fa01e1..1a0d660390d15 100644 --- a/htdocs/projet/element.php +++ b/htdocs/projet/element.php @@ -721,8 +721,6 @@ $elementuser = new User($db); - - $showdatefilter = 0; // Show the filter on date on top of element list if (!$showdatefilter) { @@ -747,6 +745,237 @@ } +// COUFFIGNAL ADD-ON +// Sample data: nodes and links arrays +require_once DOL_DOCUMENT_ROOT.'/couffignal/ProjectNodeView.php'; +// Utils +$filterExcludeKeys = fn($arr) => array_diff_key($arr, array_flip(['obj'])); + +$seeds = ProjectNodeView::getCycleSeeds($db, $object, $listofreferent['invoice'], $dates, $datee); + +$nodes = []; +$links = []; +foreach ($seeds as $seed) { + // Initialize invoice nodes and links + $inv_nodes = ProjectNodeView::loadInvoicesNodes($seed, $links); + + // Add Orders nodes + $order_nodes = ProjectNodeView::loadOrdersNodes($db, $inv_nodes, $links); + + // Add Supplier_invoices + $su_inv_nodes = ProjectNodeView::loadSupplierInvoicesNodes($db, $inv_nodes, $links); + + // Add Supplier_orders + $su_order_nodes = ProjectNodeView::loadSupplierOrdersNodes($su_inv_nodes, $links); + + // Concat nodes + $nodes = array_merge($nodes, $inv_nodes, $order_nodes, $su_inv_nodes, $su_order_nodes); +} + +// Missing : Refacto +$unlinked_nodes = []; +foreach ($listofreferent as $key => $properties) { + if (in_array($properties['class'], array_keys(ProjectNodeView::MANAGED_ELEMENTS))) { + $list_ids = $object->get_element_list($key, $properties['table'], $properties['datefieldname'], $dates, $datee, 'fk_projet'); + foreach ($list_ids as $k => $id) { + $obj = new $properties['class']($db); + $obj->fetch($id); + $unlinked_nodes[] = ProjectNodeView::loadNode($obj); + } + } +} + +// Concat & clean nodes +$nodes = array_merge($nodes, $unlinked_nodes); +$nodes_wo_obj = array_map($filterExcludeKeys, array_values($nodes)); +$clean_unique_nodes = array_map("unserialize", array_unique(array_map("serialize", $nodes_wo_obj))); + +// Clean links +$links = array_map("unserialize", array_unique(array_map("serialize", $links))); + +// Encode data as JSON for JS +$nodesJson = json_encode(array_values($clean_unique_nodes)); +$linksJson = json_encode(array_values($links)); +$colorJson = json_encode(array_combine(array_keys(ProjectNodeView::MANAGED_ELEMENTS), array_map(fn($v) => $v['color'], ProjectNodeView::MANAGED_ELEMENTS))); +?> + + + + + +

trans('GraphVisualizationTitle');?>

+
+ +
+ + + +"Description", 'checked'=>0, 'position'=>19); $arrayfields['s.nom'] = array('label'=>$langs->trans("ThirdParty"), 'checked'=>1, 'position'=>21, 'enabled'=>(!isModEnabled('societe') ? 0 : 1)); $arrayfields['s.name_alias'] = array('label'=>"AliasNameShort", 'checked'=>0, 'position'=>22); $arrayfields['commercial'] = array('label'=>$langs->trans("SaleRepresentativesOfThirdParty"), 'checked'=>0, 'position'=>23); @@ -472,7 +473,7 @@ $selectedfields = $form->multiSelectArrayWithCheckbox('selectedfields', $arrayfields, $varpage); // This also change content of $arrayfields $distinct = 'DISTINCT'; // We add distinct until filter on contact of project or task is implemented with AND EXISTS -$sql = "SELECT ".$distinct." p.rowid as id, p.ref, p.title, p.fk_statut as status, p.fk_opp_status, p.public, p.fk_user_creat,"; +$sql = "SELECT ".$distinct." p.rowid as id, p.ref, p.title, p.description, p.fk_statut as status, p.fk_opp_status, p.public, p.fk_user_creat,"; $sql .= " p.datec as date_creation, p.dateo as date_start, p.datee as date_end, p.opp_amount, p.opp_percent, (p.opp_amount*p.opp_percent/100) as opp_weighted_amount, p.tms as date_update, p.budget_amount,"; $sql .= " p.usage_opportunity, p.usage_task, p.usage_bill_time, p.usage_organize_event,"; $sql .= " p.email_msgid, p.import_key,"; @@ -1185,6 +1186,13 @@ print ''; print ''; } +// Project description +if (!empty($arrayfields['p.description']['checked'])) { + print '
'; +} // Third party if (!empty($arrayfields['s.nom']['checked'])) { print ''; + if (!$i) { + $totalarray['nbfield']++; + } + } // Company if (!empty($arrayfields['s.nom']['checked'])) { print '
'; + //search_description to be coded + //print ''; + print ''; @@ -1399,6 +1407,10 @@ print_liste_field_titre($arrayfields['p.title']['label'], $_SERVER["PHP_SELF"], "p.title", "", $param, "", $sortfield, $sortorder); $totalarray['nbfield']++; } +if (!empty($arrayfields['p.description']['checked'])) { + print_liste_field_titre($arrayfields['p.description']['label'], $_SERVER["PHP_SELF"], "p.description", "", $param, "", $sortfield, $sortorder); + $totalarray['nbfield']++; +} if (!empty($arrayfields['s.nom']['checked'])) { print_liste_field_titre($arrayfields['s.nom']['label'], $_SERVER["PHP_SELF"], "s.nom", "", $param, "", $sortfield, $sortorder); $totalarray['nbfield']++; @@ -1678,6 +1690,15 @@ $totalarray['nbfield']++; } } + // Description + if (!empty($arrayfields['p.description']['checked'])) { + print ''; + print dolPrintHTML($obj->description); + print ''; diff --git a/htdocs/projet/tasks/list.php b/htdocs/projet/tasks/list.php index c9d2553dbb2a5..175e918275b79 100644 --- a/htdocs/projet/tasks/list.php +++ b/htdocs/projet/tasks/list.php @@ -376,6 +376,22 @@ if (isset($extrafields->attributes[$object->table_element]['label']) && is_array($extrafields->attributes[$object->table_element]['label']) && count($extrafields->attributes[$object->table_element]['label'])) { $sql .= " LEFT JOIN ".MAIN_DB_PREFIX.$object->table_element."_extrafields as ef on (t.rowid = ef.fk_object)"; } +// FULLY DIRTY BY WORKING - PATAS MODULE NOT WORKING HERE +$sql .= " LEFT JOIN llx_propal as propal ON t.fk_projet = propal.fk_projet AND propal.ref_client LIKE concat('%',ef.refexpertise,'%')"; +$sql .= " LEFT JOIN llx_element_element AS pee ON (pee.fk_source = propal.rowid AND pee.sourcetype='propal' AND pee.targettype='commande' ) or (pee.fk_target = propal.rowid AND pee.targettype='propal' AND pee.sourcetype='commande' )"; +$sql .= " LEFT JOIN (select fk_source as peefs, max(fk_target) as peeft FROM llx_element_element WHERE (sourcetype='commande' and targettype='facture') GROUP BY fk_source ) peef ON pee.rowid = peef.peefs "; +$sql .= " LEFT JOIN (select fk_source as peeps , max(fk_target) as peept FROM llx_element_element WHERE (sourcetype='propal' and targettype='facture') GROUP BY fk_source ) peep ON pee.rowid = peep.peeps "; +$sql .= " LEFT JOIN (select rowid, ref_client, round(total_ht,2) as factot FROM llx_facture) fact ON fact.rowid=peef.peeft OR fact.rowid=peep.peept OR fact.rowid is null OR fact.ref_client LIKE concat('%',ef.refexpertise,'%')"; +$sql .= " LEFT JOIN llx_extralistcomp_propal as extprop ON extprop.rowid = propal.fk_statut"; +$sql .= " LEFT JOIN llx_commande_fournisseur as cdefourn ON t.fk_projet = cdefourn.fk_projet AND cdefourn.ref_supplier LIKE concat('%',ef.reffranchise,'%')"; +$sql .= " LEFT JOIN llx_element_element AS cee ON (cee.fk_source = cdefourn.rowid AND cee.targettype='invoice_supplier'AND cee.sourcetype='order_supplier' ) or (cee.fk_target = cdefourn.rowid AND cee.targettype='order_supplier' AND cee.sourcetype='invoice_supplier' )"; +$sql .= " LEFT JOIN (select fk_source as ceefs, max(fk_target) as ceeft FROM llx_element_element WHERE (sourcetype='order_supplier' and targettype='invoice_supplier') GROUP BY fk_source ) ceef ON cee.rowid = ceef.ceefs "; +//$sql .= " LEFT JOIN (select fk_source as ceeps , max(fk_target) as ceept FROM llx_element_element WHERE sourcetype='propal' and targettype='facture' GROUP BY fk_source ) ceep ON cdefourn.rowid = ceep.ceeps "; +$sql .= " LEFT JOIN (select rowid, ref_supplier, round(total_ht,2) as faftot, fk_statut FROM llx_facture_fourn) factfourn ON factfourn.rowid=ceef.ceeft OR factfourn.rowid is null OR factfourn.ref_supplier LIKE concat('%',ef.reffranchise,'%')"; +$sql .= " LEFT JOIN llx_extralistcomp_commande as extcde ON extcde.rowid = cdefourn.fk_statut "; +//$sql .= " LEFT JOIN llx_extralistcomp_commande as extfac ON extfac.rowid = factfourn.fk_statut "; +// END OF DIRTY + if ($search_project_user > 0) { $sql .= ", ".MAIN_DB_PREFIX."element_contact as ecp"; } diff --git a/htdocs/supplier_proposal/card.php b/htdocs/supplier_proposal/card.php index 5f76635fda26d..71b3d47ae85bd 100644 --- a/htdocs/supplier_proposal/card.php +++ b/htdocs/supplier_proposal/card.php @@ -1506,7 +1506,7 @@ $title = $langs->trans('ProductsAndServices'); print load_fiche_titre($title); - print '
'; + print '
'; print ''; $objectsrc->printOriginLinesList(); @@ -1916,7 +1916,7 @@ include DOL_DOCUMENT_ROOT.'/core/tpl/ajaxrow.tpl.php'; } - print '
'; + print '
'; print '
'; // Add free products/services form