Compare commits

...

5 Commits

Author SHA1 Message Date
Alexandre SPANGARO
6b19b592ad FIX Accountancy - Add validation for overlapping fiscal year dates (#36836)
* FIX Accountancy - Add validation for overlapping fiscal year dates

* Optimize

* CI
2026-01-11 13:02:31 +01:00
Laurent Destailleur
04787d8cb0 FIX icon of mastodon social network 2026-01-11 12:57:26 +01:00
Laurent Destailleur
39ed2ece8f Backport fix for #36850 2026-01-11 12:48:58 +01:00
Expresion
6d6dfb3a5e Fix numeric input parsing to support comma as decimal separator (#36845)
* Fix numeric input parsing to support comma as decimal separator

GETPOSTINT()  only handles integer values and fails when input uses a comma as decimal

* Update dispatch.php

* Change GETPOST to GETPOSTFLOAT for price input

---------

Co-authored-by: Laurent Destailleur <eldy@destailleur.fr>
2026-01-10 18:25:06 +01:00
Laurent Destailleur
608f89e882 Fix CI 2026-01-10 18:12:56 +01:00
8 changed files with 115 additions and 65 deletions

View File

@@ -128,16 +128,19 @@ if ($action == 'confirm_delete' && $confirm == "yes" && $permissiontoadd) {
$db->begin();
$id = $object->create($user);
if ($id > 0) {
$db->commit();
header("Location: ".$_SERVER["PHP_SELF"]."?id=".$id);
exit();
header("Location: " . $_SERVER["PHP_SELF"] . "?id=" . $id);
exit;
} else {
$db->rollback();
setEventMessages($object->error, $object->errors, 'errors');
// Handle overlap error
if ($id == -5 && !empty($object->errors[0])) {
setEventMessages($langs->trans($object->error, $object->errors[0]), null, 'errors');
} else {
setEventMessages($object->error, $object->errors, 'errors');
}
$action = 'create';
}
} else {
@@ -145,7 +148,7 @@ if ($action == 'confirm_delete' && $confirm == "yes" && $permissiontoadd) {
}
} else {
header("Location: ./fiscalyear.php");
exit();
exit;
}
} elseif ($action == 'update' && $permissiontoadd) {
// Update record
@@ -158,16 +161,20 @@ if ($action == 'confirm_delete' && $confirm == "yes" && $permissiontoadd) {
$object->status = GETPOSTINT('status');
$result = $object->update($user);
if ($result > 0) {
header("Location: ".$_SERVER["PHP_SELF"]."?id=".$id);
exit();
header("Location: " . $_SERVER["PHP_SELF"] . "?id=" . $id);
exit;
} else {
setEventMessages($object->error, $object->errors, 'errors');
// Handle overlap error
if ($result == -5 && !empty($object->errors[0])) {
setEventMessages($langs->trans($object->error, $object->errors[0]), null, 'errors');
} else {
setEventMessages($object->error, $object->errors, 'errors');
}
}
} else {
header("Location: ".$_SERVER["PHP_SELF"]."?id=".$id);
exit();
header("Location: " . $_SERVER["PHP_SELF"] . "?id=" . $id);
exit;
}
} elseif ($action == 'reopen' && $permissiontoadd && getDolGlobalString('ACCOUNTING_CAN_REOPEN_CLOSED_PERIOD')) {
$result = $object->fetch($id);

View File

@@ -1,8 +1,8 @@
<?php
/* Copyright (C) 2014-2025 Alexandre Spangaro <alexandre@inovea-conseil.com>
* Copyright (C) 2020 OScss-Shop <support@oscss-shop.fr>
* Copyright (C) 2023-2024 Frédéric France <frederic.france@free.fr>
* Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
/* Copyright (C) 2014-2026 Alexandre Spangaro <alexandre@inovea-conseil.com>
* Copyright (C) 2020 OScss-Shop <support@oscss-shop.fr>
* Copyright (C) 2023-2024 Frédéric France <frederic.france@free.fr>
* Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
*
* 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
@@ -135,6 +135,12 @@ class Fiscalyear extends CommonObject
$now = dol_now();
// Check for date overlaps with existing fiscal years
$checkresult = $this->checkOverlap();
if ($checkresult < 0) {
return -5; // Overlap error detected
}
$this->db->begin();
$sql = "INSERT INTO ".$this->db->prefix()."accounting_fiscalyear (";
@@ -159,16 +165,8 @@ class Fiscalyear extends CommonObject
$result = $this->db->query($sql);
if ($result) {
$this->id = $this->db->last_insert_id($this->db->prefix()."accounting_fiscalyear");
$result = $this->update($user);
if ($result > 0) {
$this->db->commit();
return $this->id;
} else {
$this->error = $this->db->lasterror();
$this->db->rollback();
return $result;
}
$this->db->commit();
return $this->id;
} else {
$this->error = $this->db->lasterror()." sql=".$sql;
$this->db->rollback();
@@ -190,6 +188,12 @@ class Fiscalyear extends CommonObject
return -1;
}
// Check for date overlaps with existing fiscal years
$checkresult = $this->checkOverlap();
if ($checkresult < 0) {
return -5; // Overlap error detected
}
$this->db->begin();
$sql = "UPDATE ".$this->db->prefix()."accounting_fiscalyear";
@@ -269,6 +273,47 @@ class Fiscalyear extends CommonObject
}
}
/**
* Check if fiscal year dates overlap with existing fiscal years
*
* @return int Return integer <0 if overlap detected, >0 if OK
*/
public function checkOverlap()
{
global $conf;
// Get entity value
$entity = (!empty($this->entity) ? $this->entity : $conf->entity);
// Query to checks if any existing fiscal year overlaps with the current date range
$sql = "SELECT label";
$sql .= " FROM " . $this->db->prefix() . "accounting_fiscalyear";
$sql .= " WHERE entity = " . ((int) $entity);
$sql .= " AND date_start <= '" . $this->db->idate($this->date_end) . "'";
$sql .= " AND date_end >= '" . $this->db->idate($this->date_start) . "'";
// Exclude current fiscal year when updating
if (!empty($this->id)) {
$sql .= " AND rowid != " . ((int) $this->id);
}
dol_syslog(get_class($this) . "::checkOverlap", LOG_DEBUG);
$result = $this->db->query($sql);
if ($result) {
if ($this->db->num_rows($result) > 0) {
$obj = $this->db->fetch_object($result);
$this->error = 'ErrorFiscalYearOverlapWithFiscalYear';
$this->errors[] = $obj->label;
return -1;
}
return 1; // No overlap found
} else {
$this->error = $this->db->lasterror();
return -2;
}
}
/**
* getTooltipContentArray
* @param array<string,mixed> $params params to construct tooltip data

View File

@@ -4252,13 +4252,13 @@ function getArrayOfSocialNetworks()
* Show social network link
*
* @param string $value Social network ID to show (only skype, without 'Name of recipient' before)
* @param int $cid Id of contact if known
* @param int $contactid Id of contact if known
* @param int $socid Id of third party if known
* @param string $type 'skype','facebook',...
* @param array<string,array{rowid:int,label:string,url:string,icon:string,active:int}> $dictsocialnetworks List of socialnetworks available
* @return string HTML Link
*/
function dol_print_socialnetworks($value, $cid, $socid, $type, $dictsocialnetworks = array())
function dol_print_socialnetworks($value, $contactid, $socid, $type, $dictsocialnetworks = array())
{
global $hookmanager, $langs, $user;
@@ -4271,61 +4271,56 @@ function dol_print_socialnetworks($value, $cid, $socid, $type, $dictsocialnetwor
if (!empty($type)) {
$htmllink = '<div class="divsocialnetwork inline-block valignmiddle">';
// Use dictionary definition for picto $dictsocialnetworks[$type]['icon']
$htmllink .= '<span class="fab pictofixedwidth '.($dictsocialnetworks[$type]['icon'] ? $dictsocialnetworks[$type]['icon'] : 'fa-link').'"></span>';
$htmllink .= '<span class="fab pictofixedwidth ' . ($dictsocialnetworks[$type]['icon'] ? $dictsocialnetworks[$type]['icon'] : 'fa-link') . '"></span>';
if ($type == 'skype') {
$htmllink .= dol_escape_htmltag($value);
$htmllink .= '&nbsp; <a href="skype:';
$htmllink .= dol_string_nospecial($value, '_', '', array('@'));
$htmllink .= '?call" alt="'.$langs->trans("Call").'&nbsp;'.$value.'" title="'.dol_escape_htmltag($langs->trans("Call").' '.$value).'">';
$htmllink .= '<img src="'.DOL_URL_ROOT.'/theme/common/skype_callbutton.png" border="0">';
$htmllink .= '?call" alt="' . $langs->trans("Call") . '&nbsp;' . $value . '" title="' . dol_escape_htmltag($langs->trans("Call") . ' ' . $value) . '">';
$htmllink .= '<img src="' . DOL_URL_ROOT . '/theme/common/skype_callbutton.png" border="0">';
$htmllink .= '</a><a href="skype:';
$htmllink .= dol_string_nospecial($value, '_', '', array('@'));
$htmllink .= '?chat" alt="'.$langs->trans("Chat").'&nbsp;'.$value.'" title="'.dol_escape_htmltag($langs->trans("Chat").' '.$value).'">';
$htmllink .= '<img class="paddingleft" src="'.DOL_URL_ROOT.'/theme/common/skype_chatbutton.png" border="0">';
$htmllink .= '?chat" alt="' . $langs->trans("Chat") . '&nbsp;' . $value . '" title="' . dol_escape_htmltag($langs->trans("Chat") . ' ' . $value) . '">';
$htmllink .= '<img class="paddingleft" src="' . DOL_URL_ROOT . '/theme/common/skype_chatbutton.png" border="0">';
$htmllink .= '</a>';
if (($cid || $socid) && isModEnabled('agenda') && $user->hasRight('agenda', 'myactions', 'create')) {
if (($contactid || $socid) && isModEnabled('agenda') && $user->hasRight('agenda', 'myactions', 'create')) {
$addlink = 'AC_SKYPE';
$link = '';
if (getDolGlobalString('AGENDA_ADDACTIONFORSKYPE')) {
$link = '<a href="'.DOL_URL_ROOT.'/comm/action/card.php?action=create&amp;backtopage=1&amp;actioncode='.$addlink.'&amp;contactid='.$cid.'&amp;socid='.$socid.'">'.img_object($langs->trans("AddAction"), "calendar").'</a>';
$link = '<a href="' . DOL_URL_ROOT . '/comm/action/card.php?action=create&backtopage=1&actioncode=' . $addlink . '&contactid=' . $contactid . '&socid=' . $socid . '">' . img_object($langs->trans("AddAction"), "calendar") . '</a>';
}
$htmllink .= ($link ? ' '.$link : '');
$htmllink .= ($link ? ' ' . $link : '');
}
} else {
$networkconstname = 'MAIN_INFO_SOCIETE_'.strtoupper($type).'_URL';
if (getDolGlobalString($networkconstname)) {
$link = str_replace('{socialid}', $value, getDolGlobalString($networkconstname));
$valuetoshow = $value;
if (preg_match('/^https?:\/\//i', $link)) {
$valuetoshow = preg_replace('/https:\/\/www\.linkedin\./', 'linkedin.', $valuetoshow);
//$valuetoshow = preg_replace('/www\.twitter\./', 'twitter.', $valuetoshow);
$htmllink .= '<a href="'.dol_sanitizeUrl($link, 0).'" target="_blank" rel="noopener noreferrer">'.dol_escape_htmltag($valuetoshow).'</a>';
} elseif ($link) {
$htmllink .= '<a href="'.dol_sanitizeUrl($link, 1).'" target="_blank" rel="noopener noreferrer">'.dol_escape_htmltag($valuetoshow).'</a>';
}
} elseif (!empty($dictsocialnetworks[$type]['url'])) {
if (!empty($dictsocialnetworks[$type]['url'])) {
$tmpvirginurl = preg_replace('/\/?{socialid}/', '', $dictsocialnetworks[$type]['url']);
if ($tmpvirginurl) {
$value = preg_replace('/^www\.'.preg_quote($tmpvirginurl, '/').'\/?/', '', $value);
$value = preg_replace('/^'.preg_quote($tmpvirginurl, '/').'\/?/', '', $value);
$value = preg_replace('/^www\.' . preg_quote($tmpvirginurl, '/') . '\/?/', '', $value);
$value = preg_replace('/^' . preg_quote($tmpvirginurl, '/') . '\/?/', '', $value);
$tmpvirginurl3 = preg_replace('/^https:\/\//i', 'https://www.', $tmpvirginurl);
if ($tmpvirginurl3) {
$value = preg_replace('/^www\.'.preg_quote($tmpvirginurl3, '/').'\/?/', '', $value);
$value = preg_replace('/^'.preg_quote($tmpvirginurl3, '/').'\/?/', '', $value);
$value = preg_replace('/^www\.' . preg_quote($tmpvirginurl3, '/') . '\/?/', '', $value);
$value = preg_replace('/^' . preg_quote($tmpvirginurl3, '/') . '\/?/', '', $value);
}
$tmpvirginurl2 = preg_replace('/^https?:\/\//i', '', $tmpvirginurl);
if ($tmpvirginurl2) {
$value = preg_replace('/^www\.'.preg_quote($tmpvirginurl2, '/').'\/?/', '', $value);
$value = preg_replace('/^'.preg_quote($tmpvirginurl2, '/').'\/?/', '', $value);
$value = preg_replace('/^www\.' . preg_quote($tmpvirginurl2, '/') . '\/?/', '', $value);
$value = preg_replace('/^' . preg_quote($tmpvirginurl2, '/') . '\/?/', '', $value);
}
}
$link = str_replace('{socialid}', $value, $dictsocialnetworks[$type]['url']);
if (preg_match('/^https?:\/\//i', $link)) {
$htmllink .= '<a href="'.dol_sanitizeUrl($link, 0).'" target="_blank" rel="noopener noreferrer">'.dol_escape_htmltag($value).'</a>';
if (preg_match('/^https?:\/\//i', $value)) {
$link = $value;
} else {
$htmllink .= '<a href="'.dol_sanitizeUrl($link, 1).'" target="_blank" rel="noopener noreferrer">'.dol_escape_htmltag($value).'</a>';
$link = str_replace('{socialid}', $value, $dictsocialnetworks[$type]['url']);
}
$valuetoshow = $value;
$valuetoshow = preg_replace('/https:\/\/www\.(twitter|x|linkedin)\.com\/?/', '', $valuetoshow);
if (preg_match('/^https?:\/\//i', $link)) {
$htmllink .= '<a href="' . dol_sanitizeUrl($link, 0) . '" target="_blank" rel="noopener noreferrer">' . dol_escape_htmltag($valuetoshow) . '</a>';
} else {
$htmllink .= '<a href="' . dol_sanitizeUrl($link, 1) . '" target="_blank" rel="noopener noreferrer">' . dol_escape_htmltag($valuetoshow) . '</a>';
}
} else {
$htmllink .= dol_escape_htmltag($value);
@@ -4340,7 +4335,7 @@ function dol_print_socialnetworks($value, $cid, $socid, $type, $dictsocialnetwor
if ($hookmanager) {
$parameters = array(
'value' => $value,
'cid' => $cid,
'cid' => $contactid,
'socid' => $socid,
'type' => $type,
'dictsocialnetworks' => $dictsocialnetworks,

View File

@@ -1194,7 +1194,7 @@ class FactureFournisseurRec extends CommonInvoice
$this->multicurrency_total_tva = empty($this->multicurrency_total_tva) ? 0 : $this->multicurrency_total_tva;
$this->multicurrency_total_ttc = empty($this->multicurrency_total_ttc) ? 0 : $this->multicurrency_total_ttc;
$pu = $price_base_type == 'HT' ? $pu_ht : $pu_ttc;
$pu = ($price_base_type == 'HT' ? $pu_ht : $pu_ttc);
// Calculate total with, without tax and tax from qty, pu, remise_percent and txtva
@@ -1211,7 +1211,7 @@ class FactureFournisseurRec extends CommonInvoice
$txtva = preg_replace('/\s*\(.*\)/', '', $txtva); // Remove code into vatrate.
}
$tabprice = calcul_price_total($qty, $pu, $remise_percent, $txtva, $txlocaltax1, $txlocaltax2, 0, $price_base_type, $info_bits, $type, $mysoc, $localtaxes_type, 100, $this->multicurrency_tx, $pu_ht_devise);
$tabprice = calcul_price_total((float) $qty, (float) $pu, $remise_percent, $txtva, $txlocaltax1, $txlocaltax2, 0, $price_base_type, $info_bits, $type, $mysoc, $localtaxes_type, 100, $this->multicurrency_tx, (float) $pu_ht_devise);
$total_ht = $tabprice[0];
$total_tva = $tabprice[1];

View File

@@ -286,7 +286,7 @@ if ($action == 'dispatch' && $permissiontoreceive) {
if (!$error && getDolGlobalString('SUPPLIER_ORDER_CAN_UPDATE_BUYINGPRICE_DURING_RECEIPT')) {
if (!isModEnabled("multicurrency") && empty($conf->dynamicprices->enabled)) {
$dto = price2num(GETPOSTINT("dto_".$reg[1].'_'.$reg[2]), '');
$dto = price2num(GETPOST("dto_".$reg[1].'_'.$reg[2]), '');
if (empty($dto)) {
$dto = 0;
}
@@ -328,7 +328,7 @@ if ($action == 'dispatch' && $permissiontoreceive) {
if (getDolGlobalString('SUPPLIER_ORDER_CAN_UPDATE_BUYINGPRICE_DURING_RECEIPT')) {
if (!isModEnabled("multicurrency") && empty($conf->dynamicprices->enabled)) {
$dto = GETPOSTINT("dto_".$reg[1].'_'.$reg[2]);
$dto = GETPOSTFLOAT("dto_".$reg[1].'_'.$reg[2]);
if (!empty($dto)) {
$unit_price = price2num((float) GETPOST("pu_".$reg[1]) * (100 - $dto) / 100, 'MU');
}
@@ -374,7 +374,7 @@ if ($action == 'dispatch' && $permissiontoreceive) {
if (!$error && getDolGlobalString('SUPPLIER_ORDER_CAN_UPDATE_BUYINGPRICE_DURING_RECEIPT')) {
if (!isModEnabled("multicurrency") && empty($conf->dynamicprices->enabled)) {
$dto = GETPOSTINT("dto_".$reg[1].'_'.$reg[2]);
$dto = GETPOSTFLOAT("dto_".$reg[1].'_'.$reg[2]);
//update supplier price
if (GETPOSTISSET($saveprice)) {
// TODO Use class

View File

@@ -329,3 +329,5 @@ ALTER TABLE llx_blockedlog ADD COLUMN debuginfo mediumtext;
ALTER TABLE llx_webhook_history ADD COLUMN trigger_code text NOT NULL;
ALTER TABLE llx_webhook_history ADD COLUMN error_message text;
ALTER TABLE llx_webhook_history MODIFY COLUMN url varchar(255);
UPDATE llx_c_socialnetworks SET icon = 'fa-mastodon' WHERE icon = '' AND code = 'mastodon';

View File

@@ -290,6 +290,7 @@ FiscalYearOpened=Fiscal year opened
FiscalYearClosed=Fiscal year closed
FiscalYearOpenedShort=Opened
FiscalYearClosedShort=Closed
ErrorFiscalYearOverlapWithFiscalYear=Fiscal year dates overlap with fiscal year: %s
ListSocialContributionAssociatedProject=List of social contributions associated with the project
DeleteFromCat=Remove from accounting group
AccountingAffectation=Accounting assignment

View File

@@ -265,7 +265,7 @@ if ($action == 'updatelines' && $permissiontoreceive) {
if (!$error && getDolGlobalString('SUPPLIER_ORDER_CAN_UPDATE_BUYINGPRICE_DURING_RECEIPT')) {
if (!isModEnabled("multicurrency") && empty($conf->dynamicprices->enabled)) {
$dto = price2num(GETPOSTINT("dto_".$reg[1].'_'.$reg[2]), '');
$dto = price2num(GETPOST("dto_".$reg[1].'_'.$reg[2]), '');
if (empty($dto)) {
$dto = 0;
}