diff --git a/.github/workflows/ci-on-release.yml b/.github/workflows/ci-on-release.yml new file mode 100644 index 00000000000..958b8865b58 --- /dev/null +++ b/.github/workflows/ci-on-release.yml @@ -0,0 +1,24 @@ +name: "CI-RELEASE" + +on: + release: + types: [published] + +jobs: + trigger-docker: + runs-on: ubuntu-latest + + steps: + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.RELEASE_DOCKER_ID }} + private-key: ${{ secrets.RELEASE_DOCKER_SECRET }} + + - uses: peter-evans/repository-dispatch@v4 + with: + token: ${{ steps.generate-token.outputs.token }} + repository: Dolibarr/dolibarr-docker + event-type: new-release + client-payload: '{"version": "${{ github.event.release.tag_name }}"}' diff --git a/htdocs/api/class/api_setup.class.php b/htdocs/api/class/api_setup.class.php index fbd951ba5b8..0e8a6c6f2ed 100644 --- a/htdocs/api/class/api_setup.class.php +++ b/htdocs/api/class/api_setup.class.php @@ -1048,7 +1048,7 @@ class Setup extends DolibarrApi global $langs; $langs->loadLangs(array('holiday')); - if (!DolibarrApiAccess::$user->hasRight('holiday', 'lire')) { + if (!DolibarrApiAccess::$user->hasRight('holiday', 'read')) { throw new RestException(403); } diff --git a/htdocs/blockedlog/admin/blockedlog.php b/htdocs/blockedlog/admin/blockedlog.php index 1ff57280382..732dfcf3c90 100644 --- a/htdocs/blockedlog/admin/blockedlog.php +++ b/htdocs/blockedlog/admin/blockedlog.php @@ -120,10 +120,9 @@ print ''.$langs->trans("BlockedLogDesc")."
country_code == 'FR') { $htmltext = $langs->trans("UnalterableLogTool1FR").'
'; + print info_admin($htmltext, 0, 0, 'warning'); } -print info_admin($htmltext, 0, 0, 'warning'); - print '
'; print '
'; // You can use div-table-responsive-no-min if you don't need reserved height for your table diff --git a/htdocs/comm/propal/card.php b/htdocs/comm/propal/card.php index e5d201387b3..e9ff4470744 100644 --- a/htdocs/comm/propal/card.php +++ b/htdocs/comm/propal/card.php @@ -19,6 +19,7 @@ * Copyright (C) 2023 William Mead * Copyright (C) 2024-2025 MDW * Copyright (C) 2024 Alexandre Spangaro + * Copyright (C) 2025 Benjamin Falière * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1956,6 +1957,12 @@ if (empty($reshook)) { $type = $product->type; $price_base_type = $product->price_base_type; + + // If base type TTc, we change pu value to define the TTC one + if ($price_base_type == 'TTC') { + $pu = $pu_ttc; + } + $label = ((GETPOST('update_label') && GETPOST('product_label')) ? GETPOST('product_label') : ''); $price_min = $product->price_min; @@ -3521,14 +3528,15 @@ if ($action == 'create') { if (empty($reshook)) { if ($action != 'editline') { // Subtotal - if ($object->status == Propal::STATUS_DRAFT && isModEnabled('subtotals') && getDolGlobalString('SUBTOTAL_TITLE_' . strtoupper($object->element))) { + if ($object->status == Propal::STATUS_DRAFT && isModEnabled('subtotals') + && (getDolGlobalInt('SUBTOTAL_TITLE_'.strtoupper($object->element)) || getDolGlobalInt('SUBTOTAL_'.strtoupper($object->element)))) { $langs->load('subtotals'); $url_button = array(); $url_button[] = array( 'lang' => 'subtotals', - 'enabled' => (isModEnabled('propal') && $object->status == Propal::STATUS_DRAFT), + 'enabled' => (isModEnabled('propal') && $object->status == Propal::STATUS_DRAFT && getDolGlobalInt('SUBTOTAL_TITLE_'.strtoupper($object->element))), 'perm' => (bool) $usercancreate, 'label' => $langs->trans('AddTitleLine'), 'url' => '/comm/propal/card.php?id=' . $object->id . '&action=add_title_line&token=' . newToken() @@ -3536,7 +3544,7 @@ if ($action == 'create') { $url_button[] = array( 'lang' => 'subtotals', - 'enabled' => (isModEnabled('propal') && $object->status == Propal::STATUS_DRAFT), + 'enabled' => (isModEnabled('propal') && $object->status == Propal::STATUS_DRAFT && getDolGlobalInt('SUBTOTAL_'.strtoupper($object->element))), 'perm' => (bool) $usercancreate, 'label' => $langs->trans('AddSubtotalLine'), 'url' => '/comm/propal/card.php?id=' . $object->id . '&action=add_subtotal_line&token=' . newToken() diff --git a/htdocs/comm/propal/class/propal.class.php b/htdocs/comm/propal/class/propal.class.php index 9de54f75831..e1925348e5b 100644 --- a/htdocs/comm/propal/class/propal.class.php +++ b/htdocs/comm/propal/class/propal.class.php @@ -18,7 +18,7 @@ * Copyright (C) 2022 ATM Consulting * Copyright (C) 2022 OpenDSI * Copyright (C) 2022 Gauthier VERDOL - * Copyright (C) 2023 William Mead + * Copyright (C) 2023 William Mead * Copyright (C) 2024-2025 MDW * * This program is free software; you can redistribute it and/or modify @@ -1879,6 +1879,8 @@ class Propal extends CommonObject $sql .= " deposit_percent = ".(!empty($this->deposit_percent) ? "'".$this->db->escape($this->deposit_percent)."'" : "null").","; $sql .= " fk_mode_reglement = ".(!empty($this->mode_reglement_id) ? (int) $this->mode_reglement_id : "null").","; $sql .= " fk_input_reason = ".(!empty($this->demand_reason_id) ? (int) $this->demand_reason_id : "null").","; + $sql .= " fk_shipping_method = ".(isset($this->shipping_method_id) ? (int) $this->shipping_method_id : "null").","; + $sql .= " fk_availability = ".(!empty($this->availability_id) ? (int) $this->availability_id : "null").","; $sql .= " note_private = ".(isset($this->note_private) ? "'".$this->db->escape($this->note_private)."'" : "null").","; $sql .= " note_public = ".(isset($this->note_public) ? "'".$this->db->escape($this->note_public)."'" : "null").","; $sql .= " model_pdf = ".(isset($this->model_pdf) ? "'".$this->db->escape($this->model_pdf)."'" : "null").","; diff --git a/htdocs/commande/card.php b/htdocs/commande/card.php index 4c88e4b7e54..a840478113e 100644 --- a/htdocs/commande/card.php +++ b/htdocs/commande/card.php @@ -3450,14 +3450,15 @@ if ($action == 'create' && $usercancreate) { } // Subtotal - if ($object->status == Commande::STATUS_DRAFT && isModEnabled('subtotals') && getDolGlobalString('SUBTOTAL_TITLE_' . strtoupper($object->element))) { + if ($object->status == Commande::STATUS_DRAFT && isModEnabled('subtotals') + && (getDolGlobalInt('SUBTOTAL_TITLE_'.strtoupper($object->element)) || getDolGlobalInt('SUBTOTAL_'.strtoupper($object->element)))) { $langs->load('subtotals'); $url_button = array(); $url_button[] = array( 'lang' => 'subtotals', - 'enabled' => (isModEnabled('order') && $object->status == Commande::STATUS_DRAFT), + 'enabled' => (isModEnabled('order') && $object->status == Commande::STATUS_DRAFT && getDolGlobalInt('SUBTOTAL_TITLE_'.strtoupper($object->element))), 'perm' => (bool) $usercancreate, 'label' => $langs->trans('AddTitleLine'), 'url' => '/commande/card.php?id=' . $object->id . '&action=add_title_line&token=' . newToken() @@ -3465,7 +3466,7 @@ if ($action == 'create' && $usercancreate) { $url_button[] = array( 'lang' => 'subtotals', - 'enabled' => (isModEnabled('order') && $object->status == Commande::STATUS_DRAFT), + 'enabled' => (isModEnabled('order') && $object->status == Commande::STATUS_DRAFT && getDolGlobalInt('SUBTOTAL_'.strtoupper($object->element))), 'perm' => (bool) $usercancreate, 'label' => $langs->trans('AddSubtotalLine'), 'url' => '/commande/card.php?id=' . $object->id . '&action=add_subtotal_line&token=' . newToken() diff --git a/htdocs/compta/facture/card-rec.php b/htdocs/compta/facture/card-rec.php index 11813560d86..17f635f41d2 100644 --- a/htdocs/compta/facture/card-rec.php +++ b/htdocs/compta/facture/card-rec.php @@ -2076,14 +2076,15 @@ if ($action == 'create') { ); // Subtotal - if (empty($object->suspended) && isModEnabled('subtotals') && getDolGlobalString('SUBTOTAL_TITLE_'.strtoupper($object->element))) { + if (empty($object->suspended) && isModEnabled('subtotals') + && (getDolGlobalInt('SUBTOTAL_TITLE_'.strtoupper($object->element)) || getDolGlobalInt('SUBTOTAL_'.strtoupper($object->element)))) { $langs->load("subtotals"); $url_button = array(); $url_button[] = array( 'lang' => 'subtotals', - 'enabled' => (isModEnabled('invoice') && $object->status == Facture::STATUS_DRAFT), + 'enabled' => (isModEnabled('invoice') && $object->status == Facture::STATUS_DRAFT && getDolGlobalInt('SUBTOTAL_TITLE_'.strtoupper($object->element))), 'perm' => (bool) $usercancreate, 'label' => $langs->trans('AddTitleLine'), 'url' => '/compta/facture/card-rec.php?id='.$object->id.'&action=add_title_line&token='.newToken() @@ -2091,7 +2092,7 @@ if ($action == 'create') { $url_button[] = array( 'lang' => 'subtotals', - 'enabled' => (isModEnabled('invoice') && $object->status == Facture::STATUS_DRAFT), + 'enabled' => (isModEnabled('invoice') && $object->status == Facture::STATUS_DRAFT && getDolGlobalInt('SUBTOTAL_'.strtoupper($object->element))), 'perm' => (bool) $usercancreate, 'label' => $langs->trans('AddSubtotalLine'), 'url' => '/compta/facture/card-rec.php?id='.$object->id.'&action=add_subtotal_line&token='.newToken() diff --git a/htdocs/compta/facture/card.php b/htdocs/compta/facture/card.php index e02cba5e602..1b7754523cb 100644 --- a/htdocs/compta/facture/card.php +++ b/htdocs/compta/facture/card.php @@ -6560,14 +6560,15 @@ if ($action == 'create') { } // Subtotal - if ($object->status == Facture::STATUS_DRAFT && isModEnabled('subtotals') && getDolGlobalString('SUBTOTAL_TITLE_'.strtoupper($object->element))) { + if ($object->status == Facture::STATUS_DRAFT && isModEnabled('subtotals') + && (getDolGlobalInt('SUBTOTAL_TITLE_'.strtoupper($object->element)) || getDolGlobalInt('SUBTOTAL_'.strtoupper($object->element)))) { $langs->load("subtotals"); $url_button = array(); $url_button[] = array( 'lang' => 'subtotals', - 'enabled' => (isModEnabled('invoice') && $object->status == Facture::STATUS_DRAFT), + 'enabled' => (isModEnabled('invoice') && $object->status == Facture::STATUS_DRAFT && getDolGlobalInt('SUBTOTAL_TITLE_'.strtoupper($object->element))), 'perm' => (bool) $usercancreate, 'label' => $langs->trans('AddTitleLine'), 'url' => '/compta/facture/card.php?facid='.$object->id.'&action=add_title_line&token='.newToken() @@ -6575,7 +6576,7 @@ if ($action == 'create') { $url_button[] = array( 'lang' => 'subtotals', - 'enabled' => (isModEnabled('invoice') && $object->status == Facture::STATUS_DRAFT), + 'enabled' => (isModEnabled('invoice') && $object->status == Facture::STATUS_DRAFT && getDolGlobalInt('SUBTOTAL_'.strtoupper($object->element))), 'perm' => (bool) $usercancreate, 'label' => $langs->trans('AddSubtotalLine'), 'url' => '/compta/facture/card.php?facid='.$object->id.'&action=add_subtotal_line&token='.newToken() diff --git a/htdocs/compta/facture/class/factureligne.class.php b/htdocs/compta/facture/class/factureligne.class.php index da8919e97f5..2bcde94a7a9 100644 --- a/htdocs/compta/facture/class/factureligne.class.php +++ b/htdocs/compta/facture/class/factureligne.class.php @@ -1070,4 +1070,38 @@ class FactureLigne extends CommonInvoiceLine return $cumulated_percent; } } + + /** + * Determines if we are using situation invoices. + * If so, determines if we are using the new mode (2) or legacy mode (1). + * + * Legacy mode means invoice line fields store the state of the cycle at the current + * situation (a cumulative value) rather than the delta between the previous situation + * and the current one. In that case, we need a ratio to convert those values. + * + * New mode = the values on the line already represent the delta between the previous + * state and the current state, so we don't need a conversion (we return 1). + * + * @return float + */ + public function getSituationRatio() + { + if (getDolGlobalInt('INVOICE_USE_SITUATION') === 1) { + // in legacy mode, the situation invoice line stores the (cumulative) state of the + // cycle at the current situation. To get the delta, we need to subtract the + // state at the previous situation (if applicable). + $prevProgress = $this->get_prev_progress($this->fk_facture); + + if ($this->situation_percent == 0) { + // should not happen + return 0; + } + + return ($this->situation_percent - $prevProgress) / $this->situation_percent; + } + // new mode (INVOICE_USE_SITUATION == 2): + // no ratio needed (data stored on line is already a delta) + // or not a situation invoice: no ratio needed either + return 1; + } } diff --git a/htdocs/compta/journal/sellsjournal.php b/htdocs/compta/journal/sellsjournal.php index 22474c15227..48beaa1933e 100644 --- a/htdocs/compta/journal/sellsjournal.php +++ b/htdocs/compta/journal/sellsjournal.php @@ -203,12 +203,22 @@ if ($result) { $line->fetch($obj->id); // id of line $prev_progress = 0; if ($obj->situation_cycle_ref > 0) { // It is a situation invoice + $prev_progress = $line->get_prev_progress($obj->rowid); // id on invoice + if (getDolGlobalInt('INVOICE_USE_SITUATION') === 1) { + // backward compat: old behavior => line's situation_percent was cumulative + // (it reflected the line's progress state, not the line progress delta) + $progressDelta = $obj->situation_percent - $prev_progress; + $progressState = $obj->situation_percent; + } else { + $progressDelta = $obj->situation_percent; + $progressState = $prev_progress + $progressDelta; + } + // Avoid divide by 0 - if ($obj->situation_percent == 0) { + if ($progressState == 0) { $situation_ratio = 0; } else { - $prev_progress = $line->get_prev_progress($obj->rowid); // id on invoice - $situation_ratio = ($obj->situation_percent - $prev_progress) / $obj->situation_percent; + $situation_ratio = $progressDelta / $progressState; } } else { $situation_ratio = 1; diff --git a/htdocs/contrat/class/api_contracts.class.php b/htdocs/contrat/class/api_contracts.class.php index 45c35e6d7b3..80083fe486e 100644 --- a/htdocs/contrat/class/api_contracts.class.php +++ b/htdocs/contrat/class/api_contracts.class.php @@ -84,7 +84,7 @@ class Contracts extends DolibarrApi } if (!DolibarrApi::_checkAccessToResource('contrat', $this->contract->id)) { - throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + throw new RestException(403, 'Access to this contract is not allowed for login '.DolibarrApiAccess::$user->login); } $this->contract->fetchObjectLinked(); @@ -175,7 +175,6 @@ class Contracts extends DolibarrApi $sql .= $this->db->plimit($limit + 1, $offset); } - dol_syslog("API Rest request"); $result = $this->db->query($sql); if ($result) { @@ -234,8 +233,19 @@ class Contracts extends DolibarrApi { global $conf; if (!DolibarrApiAccess::$user->hasRight('contrat', 'creer')) { - throw new RestException(403, "Insufficient rights"); + throw new RestException(403, "Missing permission: Create/modify contracts/subscriptions"); } + + $socid = (int) $request_data['socid']; + $thirdpartytmp = new Societe($this->db); + $thirdparty_result = $thirdpartytmp->fetch($socid); + if ($thirdparty_result < 1) { + throw new RestException(404, 'Thirdparty with id='.$socid.' not found or not allowed'); + } + if (!DolibarrApi::_checkAccessToResource('societe', $thirdpartytmp->id)) { + throw new RestException(404, 'Thirdparty with id='.$thirdpartytmp->id.' not found or not allowed'); + } + // Check mandatory fields $result = $this->_validate($request_data); @@ -308,7 +318,7 @@ class Contracts extends DolibarrApi } if (!DolibarrApi::_checkAccessToResource('contrat', $this->contract->id)) { - throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + throw new RestException(403, 'Access to this contract is not allowed for login '.DolibarrApiAccess::$user->login); } $obj_ret = []; @@ -340,7 +350,6 @@ class Contracts extends DolibarrApi $sql .= $this->db->plimit($limit + 1, $offset); } - dol_syslog("API Rest request"); $result = $this->db->query($sql); if ($result) { $num = $this->db->num_rows($result); @@ -400,7 +409,7 @@ class Contracts extends DolibarrApi } if (!DolibarrApi::_checkAccessToResource('contrat', $this->contract->id)) { - throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + throw new RestException(403, 'Access to this contract is not allowed for login '.DolibarrApiAccess::$user->login); } $request_data = (object) $request_data; @@ -460,7 +469,7 @@ class Contracts extends DolibarrApi } if (!DolibarrApi::_checkAccessToResource('contrat', $this->contract->id)) { - throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + throw new RestException(403, 'Access to this contract is not allowed for login '.DolibarrApiAccess::$user->login); } $request_data = (object) $request_data; @@ -611,7 +620,7 @@ class Contracts extends DolibarrApi } if (!DolibarrApi::_checkAccessToResource('contrat', $this->contract->id)) { - throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + throw new RestException(403, 'Access to this contract is not allowed for login '.DolibarrApiAccess::$user->login); } $updateRes = $this->contract->active_line(DolibarrApiAccess::$user, $lineid, (int) $datestart, $dateend, $comment); @@ -652,7 +661,7 @@ class Contracts extends DolibarrApi } if (!DolibarrApi::_checkAccessToResource('contrat', $this->contract->id)) { - throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + throw new RestException(403, 'Access to this contract is not allowed for login '.DolibarrApiAccess::$user->login); } $updateRes = $this->contract->close_line(DolibarrApiAccess::$user, $lineid, (int) $datestart, $comment); @@ -692,7 +701,7 @@ class Contracts extends DolibarrApi } if (!DolibarrApi::_checkAccessToResource('contrat', $this->contract->id)) { - throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + throw new RestException(403, 'Access to this contract is not allowed for login '.DolibarrApiAccess::$user->login); } // TODO Check the lineid $lineid is a line of object @@ -727,13 +736,24 @@ class Contracts extends DolibarrApi if ($id == 0) { throw new RestException(400, 'No contract with id=0 can exist'); } + $result = $this->contract->fetch($id); if (!$result) { throw new RestException(404, 'Contrat not found'); } + $old_socid = $this->contract->socid; + $oldthirdpartytmp = new Societe($this->db); + $old_thirdparty_result = $oldthirdpartytmp->fetch($old_socid); + if ($old_thirdparty_result < 1) { + throw new RestException(404, 'Thirdparty with id='.$old_socid.' not found or not allowed'); + } + if (!DolibarrApi::_checkAccessToResource('societe', $old_socid)) { + throw new RestException(403, 'Access to old thirdparty='.$old_socid.' is not allowed for login '.DolibarrApiAccess::$user->login); + } + if (!DolibarrApi::_checkAccessToResource('contrat', $this->contract->id)) { - throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + throw new RestException(403, 'Access to this contract is not allowed for login '.DolibarrApiAccess::$user->login); } foreach ($request_data as $field => $value) { if ($field == 'id') { @@ -763,6 +783,18 @@ class Contracts extends DolibarrApi continue; } + if ($field == 'socid') { + $new_socid = (int) $value; + $loopthirdpartytmp = new Societe($this->db); + $new_thirdparty_result = $loopthirdpartytmp->fetch($new_socid); + if ($new_thirdparty_result < 1) { + throw new RestException(404, 'Thirdparty with id='.$new_socid.' not found or not allowed'); + } + if (!DolibarrApi::_checkAccessToResource('societe', $new_socid)) { + throw new RestException(403, 'Access to new thirdparty='.$new_socid.' is not allowed for login '.DolibarrApiAccess::$user->login); + } + } + $this->contract->$field = $this->_checkValForAPI($field, $value, $this->contract); } @@ -790,7 +822,7 @@ class Contracts extends DolibarrApi public function delete($id) { if (!DolibarrApiAccess::$user->hasRight('contrat', 'supprimer')) { - throw new RestException(403); + throw new RestException(403, 'Missing permission: Delete contracts/subscriptions'); } if ($id == 0) { throw new RestException(400, 'No contract with id=0 can exist'); @@ -801,7 +833,7 @@ class Contracts extends DolibarrApi } if (!DolibarrApi::_checkAccessToResource('contrat', $this->contract->id)) { - throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + throw new RestException(403, 'Access to this contract is not allowed for login '.DolibarrApiAccess::$user->login); } if (!$this->contract->delete(DolibarrApiAccess::$user)) { @@ -854,7 +886,7 @@ class Contracts extends DolibarrApi } if (!DolibarrApi::_checkAccessToResource('contrat', $this->contract->id)) { - throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + throw new RestException(403, 'Access to this contract is not allowed for login '.DolibarrApiAccess::$user->login); } $result = $this->contract->validate(DolibarrApiAccess::$user, '', $notrigger); @@ -911,7 +943,7 @@ class Contracts extends DolibarrApi } if (!DolibarrApi::_checkAccessToResource('contrat', $this->contract->id)) { - throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); + throw new RestException(403, 'Access to this contract is not allowed for login '.DolibarrApiAccess::$user->login); } $result = $this->contract->closeAll(DolibarrApiAccess::$user, $notrigger); diff --git a/htdocs/core/lib/functions.lib.php b/htdocs/core/lib/functions.lib.php index 14b6cbe5b84..73a38a7c48a 100644 --- a/htdocs/core/lib/functions.lib.php +++ b/htdocs/core/lib/functions.lib.php @@ -1266,7 +1266,7 @@ function GETPOSTDATE($prefix, $hourTime = '', $gm = 'auto', $saverestore = '') if ($hourTime === 'getpost' || $hourTime === 'getpostend') { $hour = (GETPOSTISSET($prefix . 'hour') && GETPOSTINT($prefix . 'hour') >= 0) ? GETPOSTINT($prefix . 'hour') : ($hourTime === 'getpostend' ? 23 : 0); $minute = (GETPOSTISSET($prefix . 'min') && GETPOSTINT($prefix . 'min') >= 0) ? GETPOSTINT($prefix . 'min') : ($hourTime === 'getpostend' ? 59 : 0); - $second = (GETPOSTISSET($prefix . 'second') && GETPOSTINT($prefix . 'second') >= 0) ? GETPOSTINT($prefix . 'second') : ($hourTime === 'getpostend' ? 59 : 0); + $second = (GETPOSTISSET($prefix . 'sec') && GETPOSTINT($prefix . 'sec') >= 0) ? GETPOSTINT($prefix . 'sec') : ($hourTime === 'getpostend' ? 59 : 0); } elseif (preg_match('/^(\d\d):(\d\d):(\d\d)$/', $hourTime, $m)) { $hour = intval($m[1]); $minute = intval($m[2]); @@ -7584,7 +7584,7 @@ function price($amount, $form = 0, $outlangs = '', $trunc = 1, $rounding = -1, $ //print $amount."-"; $data = explode('.', $amount); $decpart = isset($data[1]) ? $data[1] : ''; - $decpart = preg_replace('/0+$/i', '', $decpart); // Supprime les 0 de fin de partie decimale + $decpart = preg_replace('/0+$/i', '', $decpart); // Remove 0 at end of decimal part //print "decpart=".$decpart."
"; $end = ''; @@ -12719,6 +12719,7 @@ function printCommonFooter($zone = 'private') $parameters = array('zone' => $zone); $tmpobject = null; + // @phan-suppress-next-line PhanPluginConstantVariableNull $reshook = $hookmanager->executeHooks('printCommonFooter', $parameters, $tmpobject, $action); // Note that $action and $object may have been modified by some hooks if (empty($reshook)) { if (getDolGlobalString('MAIN_HTML_FOOTER')) { @@ -16833,8 +16834,8 @@ function buildParamDate($prefix, $timestamp = null, $hourTime = '', $gm = 'auto' if ($hourTime === 'getpost' || ($timestamp !== null && dol_print_date($timestamp, '%H:%M:%S') !== '00:00:00')) { $TParam = array_merge($TParam, array( $prefix . 'hour' => intval(dol_print_date($timestamp, '%H')), - $prefix . 'minute' => intval(dol_print_date($timestamp, '%M')), - $prefix . 'second' => intval(dol_print_date($timestamp, '%S')) + $prefix . 'min' => intval(dol_print_date($timestamp, '%M')), + $prefix . 'sec' => intval(dol_print_date($timestamp, '%S')) )); } diff --git a/htdocs/core/lib/pdf.lib.php b/htdocs/core/lib/pdf.lib.php index 8073d8c3dff..3e569f5d3d2 100644 --- a/htdocs/core/lib/pdf.lib.php +++ b/htdocs/core/lib/pdf.lib.php @@ -2456,15 +2456,31 @@ function pdf_getlineprogress($object, $i, $outputlangs, $hidedetails = 0, $hookm return ''; } if (empty($hidedetails) || $hidedetails > 1) { - if (getDolGlobalString('SITUATION_DISPLAY_DIFF_ON_PDF')) { + // 2 = situation_percent is non-cumulative (delta of current situation) + // 1 = (old mode): situation_percent is cumulative (state at situation) + $isCumulative = getDolGlobalInt('INVOICE_USE_SITUATION') === 1; + $showDelta = (bool) getDolGlobalInt('SITUATION_DISPLAY_DIFF_ON_PDF'); + + if ($isCumulative xor $showDelta) { + // Either: + // - old mode and we want to show a total or + // - new mode and we want to show a delta + $result = $object->lines[$i]->situation_percent; + } else { + // Either: + // - old mode but we want to show a delta or + // - new mode but we want to show a total $prev_progress = 0; if (method_exists($object->lines[$i], 'get_prev_progress')) { $prev_progress = $object->lines[$i]->get_prev_progress($object->id); } - $result = round($object->lines[$i]->situation_percent - $prev_progress, 1).'%'; - } else { - $result = round($object->lines[$i]->situation_percent, 1).'%'; + $result = $isCumulative ? + // old mode: we need to compute the delta (total - sum of previous) + $object->lines[$i]->situation_percent - $prev_progress : + // new mode: we need to compute the total (sum of previous + delta) + $prev_progress + $object->lines[$i]->situation_percent; } + $result = round($result, 1).'%'; } } return $result; @@ -2510,17 +2526,9 @@ function pdf_getlinetotalexcltax($object, $i, $outputlangs, $hidedetails = 0) } elseif (empty($hidedetails) || $hidedetails > 1) { $total_ht = (isModEnabled("multicurrency") && $object->multicurrency_tx != 1 ? $object->lines[$i]->multicurrency_total_ht : $object->lines[$i]->total_ht); if (!empty($object->lines[$i]->situation_percent) && $object->lines[$i]->situation_percent > 0) { - // TODO Remove this. The total should be saved correctly in database instead of being modified here. - $prev_progress = 0; - $progress = 1; - if (method_exists($object->lines[$i], 'get_prev_progress')) { - $prev_progress = $object->lines[$i]->get_prev_progress($object->id); - $progress = ($object->lines[$i]->situation_percent - $prev_progress) / 100; - } - $result .= price($sign * ($total_ht / ($object->lines[$i]->situation_percent / 100)) * $progress, 0, $outputlangs); - } else { - $result .= price($sign * $total_ht, 0, $outputlangs); + $total_ht *= $object->lines[$i]->getSituationRatio(); } + $result .= price($sign * $total_ht, 0, $outputlangs); } } return $result; @@ -2566,17 +2574,9 @@ function pdf_getlinetotalwithtax($object, $i, $outputlangs, $hidedetails = 0) } elseif (empty($hidedetails) || $hidedetails > 1) { $total_ttc = (isModEnabled("multicurrency") && $object->multicurrency_tx != 1 ? $object->lines[$i]->multicurrency_total_ttc : $object->lines[$i]->total_ttc); if (isset($object->lines[$i]->situation_percent) && $object->lines[$i]->situation_percent > 0) { - // TODO Remove this. The total should be saved correctly in database instead of being modified here. - $prev_progress = 0; - $progress = 1; - if (method_exists($object->lines[$i], 'get_prev_progress')) { - $prev_progress = $object->lines[$i]->get_prev_progress($object->id); - $progress = ($object->lines[$i]->situation_percent - $prev_progress) / 100; - } - $result .= price($sign * ($total_ttc / ($object->lines[$i]->situation_percent / 100)) * $progress, 0, $outputlangs); - } else { - $result .= price($sign * $total_ttc, 0, $outputlangs); + $total_ttc *= $object->lines[$i]->getSituationRatio(); } + $result .= price($sign * $total_ttc, 0, $outputlangs); } } return $result; diff --git a/htdocs/core/lib/price.lib.php b/htdocs/core/lib/price.lib.php index 1d39ec0c7a7..3571167be9e 100644 --- a/htdocs/core/lib/price.lib.php +++ b/htdocs/core/lib/price.lib.php @@ -458,7 +458,8 @@ function calcul_price_total($qty, $pu, $remise_percent_ligne, $txtva, $uselocalt // Allow an external module to bypass the calculation of prices $parameters = array('result' => $result); - $tmpobject = null; $tmpaction = ''; + $tmpobject = null; + $tmpaction = ''; // @phan-suppress-next-line PhanPluginConstantVariableNull $reshook = $hookmanager->executeHooks('calcul_price_total', $parameters, $tmpobject, $tmpaction); // @phan-suppress-current-line PhanPluginConstantVariableNull if ($reshook > 0 && !empty($hookmanager->resArray['result'])) { diff --git a/htdocs/core/lib/security.lib.php b/htdocs/core/lib/security.lib.php index 648d1af136b..0b2ee3d6966 100644 --- a/htdocs/core/lib/security.lib.php +++ b/htdocs/core/lib/security.lib.php @@ -1021,14 +1021,14 @@ function checkUserAccessToObject($user, array $featuresarray, $object = 0, $tabl $checkonentitydone = 0; // Array to define rules of checks to do - $check = array('adherent', 'banque', 'bom', 'don', 'mrp', 'user', 'usergroup', 'payment', 'payment_supplier', 'payment_sc', 'product', 'produit', 'service', 'produit|service', 'categorie', 'resource', 'expensereport', 'holiday', 'salaries', 'website', 'recruitment', 'chargesociales', 'knowledgemanagement'); // Test on entity only (Objects with no link to company) + $check = array('adherent', 'banque', 'bom', 'don', 'mrp', 'user', 'usergroup', 'payment', 'payment_supplier', 'payment_sc', 'product', 'produit', 'service', 'produit|service', 'categorie', 'resource', 'expensereport', 'holiday', 'salaries', 'website', 'recruitment', 'chargesociales', 'knowledgemanagement', 'stock'); // Test on entity only (Objects with no link to company) $checksoc = array('societe'); // Test for object Societe $checkparentsoc = array('agenda', 'contact', 'contrat'); // Test on entity + link to third party on field $dbt_keyfield. Allowed if link is empty (Ex: contacts...). $checkproject = array('projet', 'project'); // Test for project object $checktask = array('projet_task', 'project_task'); // Test for task object $checkhierarchy = array('expensereport', 'holiday', 'hrm'); // check permission among the hierarchy of user $checkuser = array('bookmark'); // check permission among the fk_user (must be myself or null) - $nocheck = array('barcode', 'stock', 'webhook'); // No test + $nocheck = array('barcode', 'webhook'); // No test //$checkdefault = 'all other not already defined'; // Test on entity + link to third party on field $dbt_keyfield. Not allowed if link is empty (Ex: invoice, orders...). diff --git a/htdocs/core/lib/usergroups.lib.php b/htdocs/core/lib/usergroups.lib.php index 0a164de2404..f1e24d565cb 100644 --- a/htdocs/core/lib/usergroups.lib.php +++ b/htdocs/core/lib/usergroups.lib.php @@ -524,7 +524,7 @@ function showSkins($fuser, $edit = 0, $foruserprofile = false) print ''.$langs->trans("DarkThemeMode").''; print ''; if ($edit) { - print $form->selectarray('THEME_DARKMODEENABLED', $listofdarkmodes, getDolGlobalInt('THEME_DARKMODEENABLED'), 0, 0, 0, '', 0, 0, 0, '', 'minwidth150 maxwidth250'); + print $form->selectarray('THEME_DARKMODEENABLED', $listofdarkmodes, getDolGlobalInt('THEME_DARKMODEENABLED'), 0, 0, 0, '', 0, 0, 0, '', 'minwidth200 maxwidth250'); } else { print $listofdarkmodes[getDolGlobalInt('THEME_DARKMODEENABLED')]; } @@ -624,19 +624,19 @@ function showSkins($fuser, $edit = 0, $foruserprofile = false) if (getDolGlobalString('THEME_ELDY_USEBORDERONTABLE')) { $listofborderradius = array( - 0 => $langs->transnoentitiesnoconv("No"), - 4 => $langs->transnoentitiesnoconv("Size").' 4', - 6 => $langs->transnoentitiesnoconv("Size").' 6', - 8 => $langs->transnoentitiesnoconv("Size").' 8', - 10 => $langs->transnoentitiesnoconv("Size").' 10', - 20 => $langs->transnoentitiesnoconv("Size").' 20', + 0 => array('label' => $langs->transnoentitiesnoconv("NoRoundedCorners")), + 4 => array('label' => $langs->transnoentitiesnoconv("CornerRadius").' 4'), + 6 => array('label' => $langs->transnoentitiesnoconv("CornerRadius").' 6'), + 8 => array('label' => $langs->transnoentitiesnoconv("CornerRadius").' 8'), + 10 => array('label' => $langs->transnoentitiesnoconv("CornerRadius").' 10'), + 20 => array('label' => $langs->transnoentitiesnoconv("CornerRadius").' 20'), ); print '     '; if ($edit) { - print $form->selectarray('THEME_ELDY_BORDER_RADIUS', $listofborderradius, getDolGlobalInt('THEME_ELDY_BORDER_RADIUS'), 0, 0, 0, '', 0, 0, 0, '', 'widthcentpercentminusx maxwidth100'); + print $form->selectarray('THEME_ELDY_BORDER_RADIUS', $listofborderradius, getDolGlobalInt('THEME_ELDY_BORDER_RADIUS'), 0, 0, 0, '', 0, 0, 0, '', 'miwdith150 widthcentpercentminusx maxwidth200'); } else { - print $listofborderradius[getDolGlobalInt('THEME_ELDY_BORDER_RADIUS')]; + print $listofborderradius[getDolGlobalInt('THEME_ELDY_BORDER_RADIUS')]['label']; } //print $form->textwithpicto('', $langs->trans("NotSupportedByAllThemes"), 1, 'help', 'inline-block'); } diff --git a/htdocs/core/tpl/subtotal_create.tpl.php b/htdocs/core/tpl/subtotal_create.tpl.php index 68076823096..88d60291677 100644 --- a/htdocs/core/tpl/subtotal_create.tpl.php +++ b/htdocs/core/tpl/subtotal_create.tpl.php @@ -50,14 +50,14 @@ if ($type == 'title') { $formquestion = array( array('type' => 'text', 'name' => 'subtotallinedesc', 'label' => $langs->trans("SubtotalLineDesc"), 'moreattr' => 'placeholder="' . $langs->trans("Description") . '"'), array('type' => 'select', 'name' => 'subtotallinelevel', 'label' => $langs->trans("SubtotalLineLevel"), 'values' => $depth_array, 'default' => 1, 'select_show_empty' => 0), - array('type' => 'checkbox', 'value' => false, 'name' => 'titleshowuponpdf', 'label' => $langs->trans("ShowUPOnPDF")), - array('type' => 'checkbox', 'value' => false, 'name' => 'titleshowtotalexludingvatonpdf', 'label' => $langs->trans("ShowTotalExludingVATOnPDF")), + array('type' => 'checkbox', 'value' => true, 'name' => 'titleshowuponpdf', 'label' => $langs->trans("ShowUPOnPDF")), + array('type' => 'checkbox', 'value' => true, 'name' => 'titleshowtotalexludingvatonpdf', 'label' => $langs->trans("ShowTotalExludingVATOnPDF")), array('type' => 'checkbox', 'value' => false, 'name' => 'titleforcepagebreak', 'label' => $langs->trans("ForcePageBreak")), ); } elseif ($type == 'subtotal') { $formquestion = array( array('type' => 'select', 'name' => 'subtotaltitleline', 'label' => $langs->trans("CorrespondingTitleLine"), 'values' => $titles, 'select_show_empty' => 0), - array('type' => 'checkbox', 'value' => false, 'name' => 'subtotalshowtotalexludingvatonpdf', 'label' => $langs->trans("ShowTotalExludingVATOnPDF")), + array('type' => 'checkbox', 'value' => true, 'name' => 'subtotalshowtotalexludingvatonpdf', 'label' => $langs->trans("ShowTotalExludingVATOnPDF")), ); } diff --git a/htdocs/expedition/card.php b/htdocs/expedition/card.php index a1018a8f10e..3e1d15d09f0 100644 --- a/htdocs/expedition/card.php +++ b/htdocs/expedition/card.php @@ -329,7 +329,7 @@ if (empty($reshook)) { $object->date_shipping = $date_shipping; // Sending date $object->shipping_method_id = GETPOSTINT('shipping_method_id'); $object->tracking_number = GETPOST('tracking_number', 'alpha'); - $object->note = GETPOST('note', 'restricthtml'); // deprecated + $object->note = GETPOST('note_private', 'restricthtml'); // deprecated $object->note_private = GETPOST('note_private', 'restricthtml'); $object->note_public = GETPOST('note_public', 'restricthtml'); $object->fk_incoterms = GETPOSTINT('incoterm_id'); @@ -337,8 +337,6 @@ if (empty($reshook)) { $product = new Product($db); - - // Fill array 'array_options' with data from add form $ret = $extrafields->setOptionalsFromPost(null, $object); if ($ret < 0) { @@ -371,7 +369,7 @@ if (empty($reshook)) { $object->date_shipping = $date_shipping; // Sending date $object->shipping_method_id = GETPOSTINT('shipping_method_id'); $object->tracking_number = GETPOST('tracking_number', 'alpha'); - $object->note = GETPOST('note', 'restricthtml'); // deprecated + $object->note = GETPOST('note_private', 'restricthtml'); // deprecated $object->note_private = GETPOST('note_private', 'restricthtml'); $object->note_public = GETPOST('note_public', 'restricthtml'); $object->fk_incoterms = GETPOSTINT('incoterm_id'); diff --git a/htdocs/fichinter/card.php b/htdocs/fichinter/card.php index 280329b3f0b..79d67b2804f 100644 --- a/htdocs/fichinter/card.php +++ b/htdocs/fichinter/card.php @@ -13,6 +13,7 @@ * Copyright (C) 2023-2024 William Mead * Copyright (C) 2024-2025 MDW * Copyright (C) 2024 Alexandre Spangaro + * Copyright (C) 2025 Pierre Ardoin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/htdocs/fourn/class/fournisseur.facture-rec.class.php b/htdocs/fourn/class/fournisseur.facture-rec.class.php index 1f3e6ed01b7..4c64456847d 100644 --- a/htdocs/fourn/class/fournisseur.facture-rec.class.php +++ b/htdocs/fourn/class/fournisseur.facture-rec.class.php @@ -707,6 +707,7 @@ class FactureFournisseurRec extends CommonInvoice $this->socid = $obj->fk_soc; $this->date_creation = $obj->datec; $this->date_modification = $obj->tms; + $this->status = $obj->suspended; $this->suspended = $obj->suspended; $this->libelle = $obj->label; $this->label = $obj->label; @@ -1186,7 +1187,7 @@ class FactureFournisseurRec extends CommonInvoice return -1; } - if ($this->status == self::STATUS_SUSPENDED) { + if ($this->suspended == self::STATUS_SUSPENDED) { // Clean parameters $fk_product = empty($fk_product) ? 0 : $fk_product; $label = empty($label) ? '' : $label; diff --git a/htdocs/fourn/class/fournisseur.orderline.class.php b/htdocs/fourn/class/fournisseur.orderline.class.php index 9ea403b6c3f..fd0e91b3556 100644 --- a/htdocs/fourn/class/fournisseur.orderline.class.php +++ b/htdocs/fourn/class/fournisseur.orderline.class.php @@ -359,8 +359,8 @@ class CommandeFournisseurLigne extends CommonOrderLine } else { $sql .= "null,"; } - $sql .= "'".$this->db->escape((string) $this->product_type)."',"; - $sql .= ((int) $this->special_code) . ","; + $sql .= ((int) $this->product_type).","; + $sql .= ((int) $this->special_code).","; $sql .= "'".$this->db->escape((string) $this->rang)."',"; $sql .= "'".$this->db->escape((string) $this->qty)."', "; $sql .= " ".(empty($this->vat_src_code) ? "''" : "'".$this->db->escape((string) $this->vat_src_code)."'").","; diff --git a/htdocs/hrm/class/evaluation.class.php b/htdocs/hrm/class/evaluation.class.php index 5f509cc1a76..a2944ebbe66 100644 --- a/htdocs/hrm/class/evaluation.class.php +++ b/htdocs/hrm/class/evaluation.class.php @@ -217,7 +217,7 @@ class Evaluation extends CommonObject } if (!$user->hasRight('hrm', 'evaluation', 'readall')) { - $this->fields['fk_user']['type'] .= ':t.rowid:IN:'.$this->db->sanitize(implode(",", $user->getAllChildIds(1))); + $this->fields['fk_user']['type'] .= ' AND (t.rowid:IN:'.$this->db->sanitize(implode(",", $user->getAllChildIds(1))) .')'; } $this->date_eval = dol_now(); diff --git a/htdocs/hrm/evaluation_agenda.php b/htdocs/hrm/evaluation_agenda.php index aa767229ead..0225515b7e8 100644 --- a/htdocs/hrm/evaluation_agenda.php +++ b/htdocs/hrm/evaluation_agenda.php @@ -105,8 +105,9 @@ $permissiontoread = $user->hasRight('hrm', 'evaluation', 'read'); // Used by the // Security check (enable the most restrictive one) //if ($user->socid > 0) accessforbidden(); //if ($user->socid > 0) $socid = $user->socid; -//$isdraft = (($object->status == $object::STATUS_DRAFT) ? 1 : 0); -//restrictedArea($user, $object->module, $object->id, $object->table_element, $object->element, 'fk_soc', 'rowid', $isdraft); +$isdraft = $object->status == Evaluation::STATUS_DRAFT ? 1 : 0; +restrictedArea($user, $object->element, $object, $object->table_element, '', 'fk_soc', 'rowid', $isdraft); + if (!isModEnabled('hrm')) { accessforbidden(); } diff --git a/htdocs/hrm/evaluation_contact.php b/htdocs/hrm/evaluation_contact.php index e9ee4a84863..ae3ef778d02 100644 --- a/htdocs/hrm/evaluation_contact.php +++ b/htdocs/hrm/evaluation_contact.php @@ -71,10 +71,8 @@ $permission = $user->hasRight('hrm', 'evaluation', 'write'); // Security check (enable the most restrictive one) //if ($user->socid > 0) accessforbidden(); //if ($user->socid > 0) $socid = $user->socid; -//$isdraft = (($object->status == $object::STATUS_DRAFT) ? 1 : 0); -//restrictedArea($user, $object->element, $object->id, $object->table_element, '', 'fk_soc', 'rowid', $isdraft); -//if (empty($conf->hrm->enabled)) accessforbidden(); -//if (!$permissiontoread) accessforbidden(); +$isdraft = $object->status == Evaluation::STATUS_DRAFT ? 1 : 0; +restrictedArea($user, $object->element, $object, $object->table_element, '', 'fk_soc', 'rowid', $isdraft); diff --git a/htdocs/hrm/evaluation_document.php b/htdocs/hrm/evaluation_document.php index d9330803395..68a4401b62b 100644 --- a/htdocs/hrm/evaluation_document.php +++ b/htdocs/hrm/evaluation_document.php @@ -95,16 +95,12 @@ $permissiontoadd = $user->hasRight('hrm', 'evaluation', 'write'); // Used by th $permissiontoread = $user->hasRight('hrm', 'evaluation', 'read'); // Security check (enable the most restrictive one) -//if ($user->socid > 0) accessforbidden(); -//if ($user->socid > 0) $socid = $user->socid; -//$isdraft = (($object->status == $object::STATUS_DRAFT) ? 1 : 0); -//restrictedArea($user, $object->element, $object->id, $object->table_element, '', 'fk_soc', 'rowid', $isdraft); -if (empty($conf->hrm->enabled)) { - accessforbidden(); -} -if (!$permissiontoread) { - accessforbidden(); -} + +$isdraft = $object->status == Evaluation::STATUS_DRAFT ? 1 : 0; +restrictedArea($user, $object->element, $object, $object->table_element, '', 'fk_soc', 'rowid', $isdraft); + +if (!isModEnabled('hrm')) accessforbidden(); +if (!$permissiontoread) accessforbidden(); /* diff --git a/htdocs/hrm/evaluation_note.php b/htdocs/hrm/evaluation_note.php index 4c374e0bade..f6dfb4f17f8 100644 --- a/htdocs/hrm/evaluation_note.php +++ b/htdocs/hrm/evaluation_note.php @@ -75,10 +75,10 @@ $permissiontoread = $user->hasRight('hrm', 'evaluation', 'read'); // Used by th // Security check (enable the most restrictive one) //if ($user->socid > 0) accessforbidden(); //if ($user->socid > 0) $socid = $user->socid; -//$isdraft = (($object->status == $object::STATUS_DRAFT) ? 1 : 0); -//restrictedArea($user, $object->element, $object->id, $object->table_element, '', 'fk_soc', 'rowid', $isdraft); -//if (empty($conf->hrm->enabled)) accessforbidden(); -//if (!$permissiontoread) accessforbidden(); +$isdraft = (($object->status == Evaluation::STATUS_DRAFT) ? 1 : 0); +restrictedArea($user, $object->element, $object, $object->table_element, '', 'fk_soc', 'rowid', $isdraft); +if (empty($conf->hrm->enabled)) accessforbidden(); +if (!$permissiontoread) accessforbidden(); /* diff --git a/htdocs/imports/import.php b/htdocs/imports/import.php index 4d329b76817..0b73dd07f46 100644 --- a/htdocs/imports/import.php +++ b/htdocs/imports/import.php @@ -1898,7 +1898,7 @@ if ($step == 5 && $datatoimport) { //dol_syslog("line ".$sourcelinenb.' - '.$nboflines.' - '.$excludefirstline.' - '.$endatlinenb); $arrayrecord = $obj->import_read_record(); if ($arrayrecord === false) { - $arrayofwarnings[$sourcelinenb][0] = array('lib' => 'File has '.$nboflines.' lines. However we reach the end of file or an empty line at record '.$sourcelinenb.'. This may occurs when some records are split onto several lines and not correctly delimited by the "Char delimiter", or if there is line with no data on all fields.', 'type' => 'EOF_RECORD_ON_SEVERAL_LINES'); + $arrayofwarnings[$sourcelinenb][0] = array('lib' => $langs->trans('ErrorFileLinesReachEOF', $nboflines, $sourcelinenb), 'type' => 'EOF_RECORD_ON_SEVERAL_LINES'); $endoffile++; continue; } @@ -2321,7 +2321,7 @@ if ($step == 6 && $datatoimport) { $sourcelinenb++; $arrayrecord = $obj->import_read_record(); if ($arrayrecord === false) { - $arrayofwarnings[$sourcelinenb][0] = array('lib' => 'File has '.$nboflines.' lines. However we reach the end of file or an empty line at record '.$sourcelinenb.'. This may occurs when some records are split onto several lines and not correctly delimited by the "Char delimiter", or if there is line with no data on all fields.', 'type' => 'EOF_RECORD_ON_SEVERAL_LINES'); + $arrayofwarnings[$sourcelinenb][0] = array('lib' => $langs->trans('ErrorFileLinesReachEOF', $nboflines, $sourcelinenb), 'type' => 'EOF_RECORD_ON_SEVERAL_LINES'); $endoffile++; continue; } diff --git a/htdocs/langs/en_US/admin.lang b/htdocs/langs/en_US/admin.lang index 4b121a2a1f0..7542e7d9915 100644 --- a/htdocs/langs/en_US/admin.lang +++ b/htdocs/langs/en_US/admin.lang @@ -293,6 +293,8 @@ Orientation=Orientation SpaceX=Space X SpaceY=Space Y FontSize=Font size +NoRoundedCorners=No rounder corners +CornerRadius=Corner radius Content=Content ContentForLines=Content to display for each product or service (from variable __LINES__ of Content) NoticePeriod=Notice period diff --git a/htdocs/langs/en_US/exports.lang b/htdocs/langs/en_US/exports.lang index e701d3ae495..521e6dc136a 100644 --- a/htdocs/langs/en_US/exports.lang +++ b/htdocs/langs/en_US/exports.lang @@ -152,3 +152,4 @@ MandatoryTargetFieldsNotMapped=Some mandatory target fields are not mapped AllTargetMandatoryFieldsAreMapped=All target fields that need a mandatory value are mapped ResultOfSimulationNoError=Result of simulation: No error NumberOfLinesLimited=Number of lines limited +ErrorFileLinesReachEOF=File has %s lines. However we reach the end of file or an empty line at record %s. This may occurs when some records are split onto several lines and not correctly delimited by the "Char delimiter", or if there is line with no data on all fields. diff --git a/htdocs/margin/admin/margin.php b/htdocs/margin/admin/margin.php index 399dc03cc14..fd34f14bad6 100644 --- a/htdocs/margin/admin/margin.php +++ b/htdocs/margin/admin/margin.php @@ -74,14 +74,17 @@ if ($action == 'update') { $error = 0; if (dolibarr_set_const($db, 'MARGIN_METHODE_FOR_DISCOUNT', GETPOST('MARGIN_METHODE_FOR_DISCOUNT'), 'chaine', 0, '', $conf->entity) <= 0) { dol_print_error($db); + $error++; } if (dolibarr_set_const($db, 'MARGIN_TYPE', GETPOST('MARGIN_TYPE'), 'chaine', 0, '', $conf->entity) <= 0) { dol_print_error($db); + $error++; } if (dolibarr_set_const($db, 'AGENT_CONTACT_TYPE', GETPOST('AGENT_CONTACT_TYPE'), 'chaine', 0, '', $conf->entity) <= 0) { dol_print_error($db); + $error++; } if (!$error) { diff --git a/htdocs/product/inventory/card.php b/htdocs/product/inventory/card.php index f5d240bcd74..ec36230857f 100644 --- a/htdocs/product/inventory/card.php +++ b/htdocs/product/inventory/card.php @@ -55,9 +55,9 @@ $include_sub_warehouse = !empty(GETPOST('include_sub_warehouse')) ? GETPOST('inc $hookmanager->initHooks(array('inventorycard', 'globalcard')); // Note that conf->hooks_modules contains array if (!getDolGlobalString('MAIN_USE_ADVANCED_PERMS')) { - $result = restrictedArea($user, 'stock', $id); + $result = restrictedArea($user, 'stock', $id, 'inventory&stock'); } else { - $result = restrictedArea($user, 'stock', $id, '', 'inventory_advance'); + $result = restrictedArea($user, 'stock', $id, 'inventory&stock', 'inventory_advance'); } // Initialize a technical objects diff --git a/htdocs/product/inventory/inventory.php b/htdocs/product/inventory/inventory.php index d07977a9350..3e74f4195ac 100644 --- a/htdocs/product/inventory/inventory.php +++ b/htdocs/product/inventory/inventory.php @@ -72,9 +72,9 @@ $totalExpectedValuation = 0; $totalRealValuation = 0; $hookmanager->initHooks(array('inventorycard')); // Note that conf->hooks_modules contains array if (!getDolGlobalString('MAIN_USE_ADVANCED_PERMS')) { - $result = restrictedArea($user, 'stock', $id); + $result = restrictedArea($user, 'stock', $id, 'inventory&stock'); } else { - $result = restrictedArea($user, 'stock', $id, '', 'inventory_advance'); + $result = restrictedArea($user, 'stock', $id, 'inventory&stock', 'inventory_advance'); } // Initialize a technical objects diff --git a/htdocs/product/stock/card.php b/htdocs/product/stock/card.php index 363f39b3968..73effc43e1e 100644 --- a/htdocs/product/stock/card.php +++ b/htdocs/product/stock/card.php @@ -85,8 +85,7 @@ if (!$sortorder) { $hookmanager->initHooks(array('warehousecard', 'stocklist', 'globalcard')); // Security check -//$result=restrictedArea($user,'stock', $id, 'entrepot&stock'); -$result = restrictedArea($user, 'stock'); +$result=restrictedArea($user, 'stock', $id, 'entrepot&stock'); $object = new Entrepot($db); $extrafields = new ExtraFields($db); diff --git a/htdocs/projet/class/api_tasks.class.php b/htdocs/projet/class/api_tasks.class.php index 4bf54b81b03..a385f030a90 100644 --- a/htdocs/projet/class/api_tasks.class.php +++ b/htdocs/projet/class/api_tasks.class.php @@ -4,6 +4,7 @@ * Copyright (C) 2024-2025 Frédéric France * Copyright (C) 2025 MDW * Copyright (C) 2025 Jessica Kowal + * Copyright (C) 2025 Charlene Benke * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -572,7 +573,7 @@ class Tasks extends DolibarrApi * * @url GET {id}/getTimeSpent/{timespent_id} * - * @return TimeSpent + * @return Object data without useless information * * @throws RestException */ @@ -623,7 +624,7 @@ class Tasks extends DolibarrApi * @phan-return array{success:array{code:int,message:string}} * @phpstan-return array{success:array{code:int,message:string}} */ - public function addTimeSpent($id, $date, $duration, $product_id = null, $user_id = 0, $note = '', $progress = null) + public function addTimeSpent($id, $date, $duration, $product_id = null, $user_id = 0, $note = '', $progress = -1) { if (!DolibarrApiAccess::$user->hasRight('projet', 'creer')) { throw new RestException(403); @@ -651,8 +652,9 @@ class Tasks extends DolibarrApi $this->task->timespent_fk_product = $product_id; $this->task->timespent_fk_user = $uid; $this->task->timespent_note = $note; - if (!empty($this->task->progress)) + if (!empty($progress) && $progress >= 0 && $progress <= 100) { $this->task->progress = $progress; + } $result = $this->task->addTimeSpent(DolibarrApiAccess::$user, 0); if ($result == 0) { @@ -1113,7 +1115,4 @@ class Tasks extends DolibarrApi return $this->_cleanObjectDatas($this->task); } - - // \todo - // getSummaryOfTimeSpent } diff --git a/htdocs/projet/ganttchart.inc.php b/htdocs/projet/ganttchart.inc.php index 91f6a5ce082..f23d99ec95e 100644 --- a/htdocs/projet/ganttchart.inc.php +++ b/htdocs/projet/ganttchart.inc.php @@ -141,7 +141,7 @@ if (g.getDivId() != null) 'task_name' => $projecttmp->ref.' '.$projecttmp->title, 'task_resources' => '', 'task_start_date' => $projecttmp->date_start, - 'task_end_date' => $projecttmp->date_end, + 'task_end_date' => (!empty($projecttmp->date_end) ? $projecttmp->date_end : 0), 'task_is_group' => 1, 'task_position' => 0, 'task_css' => 'ggroupblack', 'task_milestone' => 0, 'task_parent' => 0, 'task_parent_alternate_id' => 0, 'note' => '', 'task_planned_workload' => 0 diff --git a/htdocs/projet/tasks/note.php b/htdocs/projet/tasks/note.php index bb44a662ad6..79cfb7e513e 100644 --- a/htdocs/projet/tasks/note.php +++ b/htdocs/projet/tasks/note.php @@ -88,7 +88,7 @@ if (!empty($project_ref) && !empty($withproject)) { $id = $tasksarray[0]->id; $object->fetch($id); } else { - header("Location: ".DOL_URL_ROOT.'/projet/tasks.php?id='.$projectstatic->id.($withproject ? '&withproject=1' : '').(empty($mode) ? '' : '&mode='.$mode)); + header("Location: ".DOL_URL_ROOT.'/projet/tasks.php?id='.$projectstatic->id.'&withproject=1'); exit; } } @@ -145,7 +145,8 @@ if ($object->id > 0) { $head = project_prepare_head($projectstatic); print dol_get_fiche_head($head, $tab, $langs->trans("Project"), -1, ($projectstatic->public ? 'projectpub' : 'project')); - $param = (isset($mode) && $mode == 'mine' ? '&mode=mine' : ''); + $param = ''; + // Project card $linkback = ''.$langs->trans("BackToList").''; diff --git a/htdocs/public/webportal/css/btn.css b/htdocs/public/webportal/css/btn.css new file mode 100644 index 00000000000..035d55a143f --- /dev/null +++ b/htdocs/public/webportal/css/btn.css @@ -0,0 +1,56 @@ + +/* + * BUTTON With Low emphasis + */ + +button.btn-low-emphasis{ + text-align: center; + display: inline-block; + border: none; + outline: none; + cursor: pointer; + margin: 0; + padding: 0; + width: auto; + min-width: 1.5em; + min-height: 1.5em; + line-height: 1.5em; + + overflow: visible; + background: transparent; + background-position: center; /* used for hover ripple effect */ + background-size: 0%; + color: var(--colortextlink, inherit); + font: inherit; + line-height: normal; + + /* Corrects font smoothing for webkit */ + -webkit-font-smoothing: inherit; + -moz-osx-font-smoothing: inherit; + + /* Corrects inability to style clickable input types in iOS */ + -webkit-appearance: none; + + + transition: background 0.8s;/* used for hover ripple effect */ + background: transparent radial-gradient(circle, transparent 1%, hsla(var(--colortextlink-h),var(--colortextlink-s) ,var(--colortextlink-l) , 0.1) 1%, transparent 10%) center/15000%; +} + +button.btn-low-emphasis.--btn-icon{ + border-radius: 100%; +} + +button.btn-low-emphasis :is(.fa, .fas){ + color: var(--colortextlink, inherit); + opacity: 0.4; +} + +button.btn-low-emphasis:is(:focus,:hover) :is(.fa, .fas){ + opacity: 0.8; +} + +button.btn-low-emphasis.--btn-icon:active { + background-color: hsla(var(--colortextlink-h),var(--colortextlink-s) ,var(--colortextlink-l) , 0.1); + background-size: 100%; + transition: background 0s;/* used for hover ripple effect */ +} diff --git a/htdocs/public/webportal/css/dialog.css b/htdocs/public/webportal/css/dialog.css new file mode 100644 index 00000000000..50e42c99e82 --- /dev/null +++ b/htdocs/public/webportal/css/dialog.css @@ -0,0 +1,91 @@ +/* Dialog container */ +.dialog-full-screen article { + width: 80vw; + max-width: 95vw; + height: 90vh; + max-height: 95vh; + + position: fixed; + margin: 0 auto; + + display: flex; + flex-direction: column; + flex-wrap: nowrap; + justify-content: normal; + align-items: normal; + align-content: normal; +} + +.dialog-full-screen article:has(.dialog-header) { + padding: 0; +} + + +.dialog-full-screen article .dialog-body { + display: block; + flex-grow: 2; + flex-shrink: 1; + flex-basis: auto; + align-self: auto; + order: 0; +} + +@media (min-width: 576px) { + dialog.dialog-full-screen article { + max-width: 95%; + } +} +@media (min-width: 768px) { + dialog.dialog-full-screen article { + max-width: 95%; + } +} + +/* Iframe */ +.dialog-iframe { + border: none; + width: 100%; + height: 100%; +} + + + +/*!* Header *!*/ +.dialog-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5em 1em; + border-bottom: 1px solid rgba(0,0,0,0.1); +} + +.dialog-header-title-container { + flex: 1; + overflow: hidden; +} + +.dialog-header-title { + margin: 0; + text-overflow: ellipsis; + overflow: hidden; +} + +.dialog-header-action-container { + flex: 0 0 auto; + white-space: nowrap; + display: flex; + flex-direction: row-reverse; + gap: 0.5em; +} + +/* Header title */ +.dialog-header h2 { + margin: 0; + font-size: 1.2em; +} + +/* Close button */ +dialog article .dialog-header-btn.close { + margin-top: 0; + margin-bottom: 0; +} diff --git a/htdocs/public/webportal/css/style.css.php b/htdocs/public/webportal/css/style.css.php index 84b52b4c378..1e034f2ae97 100644 --- a/htdocs/public/webportal/css/style.css.php +++ b/htdocs/public/webportal/css/style.css.php @@ -99,6 +99,8 @@ if (empty($dolibarr_nocache)) { @import "badges.css"; @import "title-buttons.css"; @import "nav.css"; +@import "dialog.css"; +@import "btn.css"; /** This file can overwrite default pico css */ diff --git a/htdocs/public/webportal/tpl/footer.tpl.php b/htdocs/public/webportal/tpl/footer.tpl.php index 75f0a7e1c3d..f4358235dfc 100644 --- a/htdocs/public/webportal/tpl/footer.tpl.php +++ b/htdocs/public/webportal/tpl/footer.tpl.php @@ -117,44 +117,60 @@ if (empty($conf->browser->layout) || $conf->browser->layout != 'phone') { ?> jQuery(document).ready(function () { jQuery(".modal_card").click(function (event) { console.log("We click on card link for element with href=" + $(this).attr('href')); - modal_card($(this).attr('href')); + const modalTitle = $(this).data('modal-title') || $(this).attr('title') || ''; + modal_card($(this).attr('href'), modalTitle); event.preventDefault(); return false; }); }); /** - * Function show modal card. It uses the "modal" function. + * Show modal card using Pico.css modal system * - * @param url Url - * @param title Dialog box title - * @return void - * @see newpopup() + * @param {string} url - URL to load in the iframe + * @param {string} title - Modal title */ - function modal_card(url, title) { - console.log("modal_card A click was done: url=" + url); + function modal_card(url, title = '') { + console.log("modal_card called: url=" + url); - let width = "85%"; - let object_width = '100%'; - let height = ($(window).height() - 60) * 0.90; - let object_height = '98%'; - let newElem = ''; + const modal = document.getElementById('modalforcard'); // Pico.css modal container + if (!modal) { + console.error('Modal container not found'); + return; + } - $('#modalforcard article').css('width', width).css('height', height).html(newElem); - let modal = document.getElementById('modalforcard'); + // Insert structured modal content + modal.querySelector('article').innerHTML = ` +
+
+

${title}

+
+
+ +
+
+
+ +
+ `; + + // Add close button handler + const closeBtn = modal.querySelector('.dialog-close-btn'); + if (closeBtn) { + closeBtn.addEventListener('click', () => close_modal_card()); + } + + // Open modal via Pico.css function openModal(modal); } /** - * Function close modal card. It uses the "modal" function. - * - * @return void + * Close modal card using Pico.css modal system */ function close_modal_card() { - console.log("close_modal_card A click was done"); + const modal = document.getElementById('modalforcard'); + if (!modal) return; - let modal = document.getElementById('modalforcard'); closeModal(modal); } @@ -215,6 +231,7 @@ if (empty($conf->browser->layout) || $conf->browser->layout != 'phone') { ?> } function show_preview(mode) { + // TODO : rebuild dialog tpl show modal_card let newElem = ''; if (mode == 'image' && showOriginalSizeButton) { @@ -252,8 +269,8 @@ if (empty($conf->browser->layout) || $conf->browser->layout != 'phone') { ?> -
-
+
+
diff --git a/htdocs/public/webportal/tpl/header.tpl.php b/htdocs/public/webportal/tpl/header.tpl.php index 07b05a30609..cd13ea7d3ce 100644 --- a/htdocs/public/webportal/tpl/header.tpl.php +++ b/htdocs/public/webportal/tpl/header.tpl.php @@ -19,6 +19,8 @@ /** * @var Context $context Object context for webportal * @var Translate $langs + * @@var string[] $vars['body-class'] more CSS classes for body + * @@var string[] $vars['body-theme'] theme css to apply default is custom */ // Protection to avoid direct call of template @@ -85,6 +87,18 @@ top_httphead(); // Common dolibarr js functions $jQueryUIJSUrl = $context->rootUrl.'js/lib_head.js.php'; print ''."\n"; + + $bodyAttributes = [ + 'data-theme' => $vars['body-theme']??'custom', + 'data-controller' => $context->controller, + ]; + + if (!empty($vars['body-class'])) { + $bodyAttributes['class'] = $vars['body-class']; + } + + $bodyCompiledAttributes = commonHtmlAttributeBuilder($bodyAttributes); + ?> - + > diff --git a/htdocs/societe/class/api_contacts.class.php b/htdocs/societe/class/api_contacts.class.php index f497704568d..17cf3cdccc4 100644 --- a/htdocs/societe/class/api_contacts.class.php +++ b/htdocs/societe/class/api_contacts.class.php @@ -22,6 +22,7 @@ use Luracast\Restler\RestException; //require_once DOL_DOCUMENT_ROOT.'/contact/class/contact.class.php'; //require_once DOL_DOCUMENT_ROOT.'/categories/class/categorie.class.php'; +require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php'; /** @@ -365,6 +366,17 @@ class Contacts extends DolibarrApi } continue; } + if ($field == 'socid') { + $new_socid = (int) $value; + $loopthirdpartytmp = new Societe($this->db); + $new_thirdparty_result = $loopthirdpartytmp->fetch($new_socid); + if ($new_thirdparty_result < 1) { + throw new RestException(404, 'Thirdparty with id='.$new_socid.' not found or not allowed'); + } + if (!DolibarrApi::_checkAccessToResource('societe', $new_socid)) { + throw new RestException(403, 'Access to socid/thirdparty='.$new_socid.' is not allowed for login '.DolibarrApiAccess::$user->login); + } + } $this->contact->$field = $this->_checkValForAPI($field, $value, $this->contact); } @@ -422,6 +434,17 @@ class Contacts extends DolibarrApi } continue; } + if ($field == 'socid') { + $new_socid = (int) $value; + $loopthirdpartytmp = new Societe($this->db); + $new_thirdparty_result = $loopthirdpartytmp->fetch($new_socid); + if ($new_thirdparty_result < 1) { + throw new RestException(404, 'Thirdparty with id='.$new_socid.' not found or not allowed'); + } + if (!DolibarrApi::_checkAccessToResource('societe', $new_socid)) { + throw new RestException(403, 'Access to socid/thirdparty='.$new_socid.' is not allowed for login '.DolibarrApiAccess::$user->login); + } + } $this->contact->$field = $this->_checkValForAPI($field, $value, $this->contact); } diff --git a/htdocs/theme/eldy/global.inc.php b/htdocs/theme/eldy/global.inc.php index b501aa9d303..2a1eae09af1 100644 --- a/htdocs/theme/eldy/global.inc.php +++ b/htdocs/theme/eldy/global.inc.php @@ -735,6 +735,9 @@ input.pageplusone { margin-right: 4px; margin-left: 3px; width: 32px; + border-top: 0; + border-left: 0; + border-right: 0; } .paginationlastpage a { padding-left: 6px; @@ -7973,8 +7976,8 @@ div.kanban.column div.ui-sortable-helper { /* ============================================================================== */ .searchpage .tagtr .tagtd { - padding-top: 2px; - padding-bottom: 2px; + padding-top: 5px; + padding-bottom: 5px; } .searchpage .tagtr .tagtd .button { background: unset; diff --git a/htdocs/theme/md/style.css.php b/htdocs/theme/md/style.css.php index 92b4e1c5297..344c51d99ff 100644 --- a/htdocs/theme/md/style.css.php +++ b/htdocs/theme/md/style.css.php @@ -911,6 +911,9 @@ input.pageplusone { margin-right: 4px; margin-left: 3px; width: 32px; + border-top: 0; + border-left: 0; + border-right: 0; } .paginationlastpage a { padding-left: 6px; diff --git a/htdocs/webportal/controllers/abstractcard.controller.class.php b/htdocs/webportal/controllers/abstractcard.controller.class.php index 709570f67f9..ccc4f507201 100644 --- a/htdocs/webportal/controllers/abstractcard.controller.class.php +++ b/htdocs/webportal/controllers/abstractcard.controller.class.php @@ -47,7 +47,10 @@ abstract class AbstractCardController extends Controller return; } - $this->loadTemplate('header'); + $this->loadTemplate('header', [ + 'body-class' => empty($this->formCard->modal) ? '' : '--is-modal' + ]); + if (empty($this->formCard->modal)) { $this->loadTemplate('menu'); $this->loadTemplate('hero-header-banner');