* FIX #39190 wrong unit price when invoicing time spent with one line per declaration (#39215) * FIX #39190 wrong unit price when invoicing time spent with one line per declaration In projet/tasks/time.php, the 'onelineperperiod' mode (invoice generation option "One line for each time spent declaration") computed the unit price as $pu_ht = price2num($value['totalvaluetodivideby3600'] / 3600, 'MU'); but totalvaluetodivideby3600 is timespent_duration (seconds) * timespent_thm (hourly rate), so dividing by 3600 yields "hours * hourly rate", which is the line total and not a unit price. The quantity passed to addline() is the number of hours, so the line was billed hours * (hours * rate) instead of hours * rate: the time is over-billed by a factor equal to the number of hours. The sibling modes are already correct: 'onelineperuser' and 'onelinepertask' divide by the recorded number of seconds. Up to 16.0 'onelineperuser' had the same /3600 expression; it was fixed there and 'onelineperperiod' was left behind. Align it with that reference implementation, including the same guard against a zero duration. Also reset $pu_ht for each generated line. $pu_ht is initialized once before the loops (with the price of the selected product/service, or 0) and this mode only had an "if (empty($pu_ht))" test, so once a price was computed for the first line every following line reused it, and lines of users with a different hourly rate got the rate of the first line. The base price is saved before the loop and restored on each iteration, so the product/service price path is preserved. * Rename variable for clarity in time.php --------- Co-authored-by: Laurent Destailleur <eldy@destailleur.fr> * FIX isInt() does not detect sized integer types (tinyint, bigint) (#39236) CommonObject::isInt() decides whether a list-page column gets numeric search mode and is also used when resolving $this->id. The regex /(^int|int$)/ missed sized integer types such as tinyint(4) and bigint(20): they neither start nor end with "int". These types are not exotic: tinyint(4) is used in several core $fields definitions (societe, contact, entrepot, companypaymentmode, ...) and ModuleBuilder-generated tables use bigint(20) for rowid, so isInt() should recognise them rather than requiring callers to change the column type. The new regex /^(?:tiny|small|medium|big)?int|int$/ also handles smallint/mediumint with a size. Anchoring is preserved on purpose so that strings which merely contain "int" (e.g. sellist:Table:...) are still NOT matched, while the Dolibarr foreign-key syntax "integer:Class:path" keeps being detected. Signed-off-by: Jam Balaya <jambalaya.pyoncafe@outlook.jp> * FIX #34667 checkUserAccessToObject: skip entity check for non-multientity objects (multicompany) (#39246) When the multicompany module is enabled, checkUserAccessToObject() (used by restrictedArea(), e.g. from an ajax tooltip) ran the entity check unconditionally in its default/custom-object branch, building "AND dbt.entity IN (...)". For a custom object whose table has no entity column (ismultientitymanaged = 0), that SQL fails, nb comes back 0 and access is wrongly denied. Guard the branch so the entity test only runs when the object is actually multi-entity managed. Keep the test for legacy classes that never define ismultientitymanaged (null/unset) and for a non-object $object, and skip it only when a class explicitly sets ismultientitymanaged = 0. Supersedes the stale #34667 (by @jyhere) and applies the condition requested by @eldy in its review. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> * FIX mo_production: show very-short unit label instead of untranslated 'UnitPShort' (#39253) The "Unit" column of the consume/produce tables used measuringUnitString(..., 2), which translates a <Label>Short key. Those keys do not exist for most units (e.g. a Piece -> 'UnitPShort', kg -> 'WeightUnitkgShort'), so the raw untranslated key was displayed. Use param 1 (the very-short DB short_label: 'p', 'kg', ...) like every other unit column in this file and across the codebase. Supersedes #31706. Signed-off-by: Jam Balaya <jambalaya.pyoncafe@outlook.jp> * FIX #32217 Fire BILL_CREATE with create-form extrafields on next situation invoice (#39254) When creating the next situation invoice, the create-form extrafields were saved AFTER createFromCurrent() had already fired BILL_CREATE, so triggers listening to BILL_CREATE never saw them. Passing 'BILL_CREATE' to insertExtraFields() (as first proposed) would fire the trigger a second time. Instead, set the extrafields on $object before createFromCurrent() and let create() insert them and fire BILL_CREATE once with the values present. createFromCurrent() now skips its fetch_optionals() when array_options is already set by the caller, so the form values are no longer overwritten by the source invoice values. The two other createFromCurrent() callers do not pre-set array_options, so their behaviour is unchanged. Supersedes #32217. Signed-off-by: Jam Balaya <jambalaya.pyoncafe@outlook.jp> * FIX #33065 page hangs when displaying a very long html content dol_escape_htmltag() protects the tags listed in $noescapetags before calling htmlentities(). For the tags carrying attributes it used a do/while loop that, on each round, searched the first '<tag ...>' with preg_match, replaced every occurrence of that exact string with str_replace, then compared the whole content with strcmp to decide whether to loop again. That loop only consumes one distinct attribute string per round, and each round scans, rebuilds and compares the whole content. With the 'common' tag list (48 tags) the cost is O(48 * number of distinct attributed tags * length), so it degrades quadratically with the size of the content while the memory usage stays flat. A long note or ticket message therefore burns CPU until max_execution_time is reached instead of raising a memory error. Replace that loop with a single preg_replace_callback() pass per tag. The replacement string contains no '<', so it can never build a new tag to protect and one pass is enough. The attribute string is transformed by exactly the same operations, in the same order, so the output is unchanged. Measured on the same content (48 tags, distinct attributes per tag), before and after, with identical output: spans length before after speedup 500 29 KB 0.044s 0.006s 7.9x 1000 59 KB 0.140s 0.010s 13.4x 2000 121 KB 0.822s 0.028s 29.1x 4000 245 KB 2.361s 0.042s 56.2x 8000 493 KB 11.813s 0.171s 69.0x Equivalence was checked on 26 hand written cases (attributes with brackets, quotes, forbidden characters, unclosed tags, '<' inside an attribute, reserved keywords, upper case tags, tabs and newlines) and on 3000 randomly generated inputs: the old and the new code return the same string for all of them. --------- Signed-off-by: Jam Balaya <jambalaya.pyoncafe@outlook.jp> Co-authored-by: Laurent Destailleur <eldy@destailleur.fr> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5529fb9592
commit
b8dc0d34ec
1 changed files with 15 additions and 10 deletions
|
|
@ -2601,21 +2601,26 @@ function dol_escape_htmltag($stringtoescape, $keepb = 0, $keepn = 0, $noescapeta
|
|||
$tmp = str_ireplace('</' . $tagtoreplace . '>', '__ENDTAGTOREPLACE' . $tagtoreplace . '__', $tmp);
|
||||
$tmp = preg_replace('/<' . preg_quote($tagtoreplace, '/') . ' \/>/', '__BEGINENDTAGTOREPLACE' . $tagtoreplace . '__', $tmp);
|
||||
|
||||
// For case of tag with attributes
|
||||
do {
|
||||
$tmpold = $tmp;
|
||||
|
||||
if (preg_match('/<' . preg_quote($tagtoreplace, '/') . '(\s+)([^>]+)>/', $tmp, $reg)) {
|
||||
// For case of tag with attributes.
|
||||
// All the occurrences are protected in a single pass: the replacement string contains no '<', so it
|
||||
// can never build a new tag to protect (a loop replacing one distinct attribute string per round was
|
||||
// rescanning the whole content for each of them, so the cost was quadratic on large contents).
|
||||
$tmp = preg_replace_callback(
|
||||
'/<'.preg_quote($tagtoreplace, '/').'(\s+)([^>]+)>/',
|
||||
/**
|
||||
* @param string[] $reg
|
||||
* @return string
|
||||
*/
|
||||
static function ($reg) use ($tagtoreplace) {
|
||||
// We want to protect the attribute part ... in '<xxx ...>' to avoid transformation by htmlentities() later
|
||||
$tmpattributes = str_ireplace(array('[', ']'), '_', $reg[2]); // We must never have [ ] inside the attribute string
|
||||
$tmpattributes = str_ireplace('"', '__DOUBLEQUOTE__', $tmpattributes);
|
||||
$tmpattributes = preg_replace('/[^a-z0-9_%,\/\?\;\s=&\.\-@:\.#\+]/i', '', $tmpattributes);
|
||||
//$tmpattributes = preg_replace("/float:\s*(left|right)/", "", $tmpattributes); // Disabled: we must not remove content
|
||||
$tmp = str_replace('<' . $tagtoreplace . $reg[1] . $reg[2] . '>', '__BEGINTAGTOREPLACE' . $tagtoreplace . '[' . $tmpattributes . ']__', $tmp);
|
||||
}
|
||||
|
||||
$diff = strcmp($tmpold, $tmp);
|
||||
} while ($diff);
|
||||
return '__BEGINTAGTOREPLACE'.$tagtoreplace.'['.$tmpattributes.']__';
|
||||
},
|
||||
$tmp
|
||||
) ?? $tmp;
|
||||
}
|
||||
|
||||
$tmp = str_ireplace('&', '__ANDNOSEMICOLON__', $tmp);
|
||||
|
|
|
|||
Loading…
Reference in a new issue