From bf5ddc4f34213ffe75d6d601c16148a3fe506455 Mon Sep 17 00:00:00 2001 From: Nik Okuntseff Date: Mon, 12 Feb 2018 15:28:45 +0000 Subject: [PATCH] Fixed rounding errors in monthly quotas. --- WEB-INF/lib/ttTeamHelper.class.php | 6 +-- WEB-INF/lib/ttTimeHelper.class.php | 66 ++++++++++++++++++++++++++++++ WEB-INF/lib/ttUser.class.php | 8 ++-- WEB-INF/resources/it.lang.php | 4 +- WEB-INF/templates/footer.tpl | 2 +- dbinstall.php | 12 ++++-- mysql.sql | 1 - plugins/MonthlyQuota.class.php | 19 ++++----- quotas.php | 17 ++++---- time.php | 4 +- 10 files changed, 103 insertions(+), 36 deletions(-) diff --git a/WEB-INF/lib/ttTeamHelper.class.php b/WEB-INF/lib/ttTeamHelper.class.php index b3089963..c0baa3d8 100644 --- a/WEB-INF/lib/ttTeamHelper.class.php +++ b/WEB-INF/lib/ttTeamHelper.class.php @@ -805,7 +805,7 @@ class ttTeamHelper { $plugins_part = ''; $config_part = ''; $lock_spec_part = ''; - $workday_hours_part = ''; + $workday_minutes_part = ''; if (isset($fields['currency'])) $currency_part = ', currency = '.$mdb2->quote($fields['currency']); if (isset($fields['lang'])) $lang_part = ', lang = '.$mdb2->quote($fields['lang']); @@ -820,11 +820,11 @@ class ttTeamHelper { if (isset($fields['plugins'])) $plugins_part = ', plugins = '.$mdb2->quote($fields['plugins']); if (isset($fields['config'])) $config_part = ', config = '.$mdb2->quote($fields['config']); if (isset($fields['lock_spec'])) $lock_spec_part = ', lock_spec = '.$mdb2->quote($fields['lock_spec']); - if (isset($fields['workday_hours'])) $workday_hours_part = ', workday_hours = '.$mdb2->quote($fields['workday_hours']); + if (isset($fields['workday_minutes'])) $workday_minutes_part = ', workday_minutes = '.$mdb2->quote($fields['workday_minutes']); $sql = "update tt_teams set $name_part $currency_part $lang_part $decimal_mark_part $date_format_part $time_format_part $week_start_part $tracking_mode_part $task_required_part $record_type_part - $uncompleted_indicators_part $bcc_email_part $plugins_part $config_part $lock_spec_part $workday_hours_part where id = $team_id"; + $uncompleted_indicators_part $bcc_email_part $plugins_part $config_part $lock_spec_part $workday_minutes_part where id = $team_id"; $affected = $mdb2->exec($sql); if (is_a($affected, 'PEAR_Error')) return false; diff --git a/WEB-INF/lib/ttTimeHelper.class.php b/WEB-INF/lib/ttTimeHelper.class.php index f5c3e938..8c3d27b6 100644 --- a/WEB-INF/lib/ttTimeHelper.class.php +++ b/WEB-INF/lib/ttTimeHelper.class.php @@ -106,6 +106,72 @@ class ttTimeHelper { return false; } + // postedDurationToMinutes - converts a value representing a duration + // (usually enetered in a form by a user) to integer number of minutes. + // + // At the moment, we have 2 variations of duration types: + // 1) A duration within a day, such as in a time entry. + // These are less or equal to 24 hours. + // + // 2) A duration of a monthly quota, with max value of 31*24 hours. + // + // This function is generic to be used for both types. + // Other functions will be used to check for specific max values. + // + // Returns false if the value cannot be converted. + static function postedDurationToMinutes($duration) { + // Handle empty value. + if (!isset($duration) || strlen($duration) == 0) + return null; // Value is not set. Caller decides whether it is valid or not. + + // Handle whole hours. + if (preg_match('/^\d{1,3}h?$/', $duration )) { // 0 - 999, 0h - 999h + $minutes = 60 * trim($duration, 'h'); + return $minutes; + } + + // Handle a normalized duration value. + if (preg_match('/^\d{1,3}:[0-5][0-9]$/', $duration )) { // 0:00 - 999:59 + $time_array = explode(':', $duration); + $minutes = (int)@$time_array[1] + ((int)@$time_array[0]) * 60; + return $minutes; + } + + // Handle localized fractional hours. + global $user; + $localizedPattern = '/^(\d{1,3})?['.$user->decimal_mark.'][0-9]{1,4}h?$/'; + if (preg_match($localizedPattern, $duration )) { // decimal values like .5, 1.25h, ... .. 999.9999h (or with comma) + if ($user->decimal_mark == ',') + $duration = str_replace (',', '.', $duration); + + $minutes = (int)round(60 * floatval($duration)); + return $minutes; + } + + // Handle minutes. Some users enter durations like 10m (meaning 10 minutes). + if (preg_match('/^\d{1,5}m$/', $duration )) { // 0m - 99999m + $minutes = (int) trim($duration, 'm'); + return $minutes; + } + + // Everything else is not a valid duration. + return false; + } + + static function durationToMinutes($duration) { + $minutes = ttTimeHelper::postedDurationToMinutes($duration); + if (false === $minutes || $minutes > 24*60) + return false; // $duration is not valid for a day entry. + return $minutes; + } + + static function quotaToMinutes($duration) { + $minutes = ttTimeHelper::postedDurationToMinutes($duration); + if (false === $minutes || $minutes > 31*24*60) + return false; // $duration is not valid for a monthly quota. + return $minutes; + } + // validateDuration - a future replacement of the isValidDuration above. // Validates a passed in $value as a time duration string in hours and / or minutes. // Returns either a normalized duration (hh:mm) or false if $value is invalid. diff --git a/WEB-INF/lib/ttUser.class.php b/WEB-INF/lib/ttUser.class.php index f04b4996..b433e6ef 100644 --- a/WEB-INF/lib/ttUser.class.php +++ b/WEB-INF/lib/ttUser.class.php @@ -41,7 +41,7 @@ class ttUser { var $date_format = null; // Date format. var $time_format = null; // Time format. var $week_start = 0; // Week start day. - var $show_holidays = 1; // Whether to show holidays in calendar. + var $show_holidays = 0; // Whether to show holidays in calendar. var $tracking_mode = 0; // Tracking mode. var $project_required = 0; // Whether project selection is required on time entires. var $task_required = 0; // Whether task selection is required on time entires. @@ -54,7 +54,7 @@ class ttUser { var $team = null; // Team name. var $custom_logo = 0; // Whether to use a custom logo for team. var $lock_spec = null; // Cron specification for record locking. - var $workday_hours = 8; // Number of work hours in a regular day. + var $workday_minutes = 480; // Number of work minutes in a regular day. var $rights = 0; // A mask of user rights. // Constructor. @@ -69,7 +69,7 @@ class ttUser { $sql = "SELECT u.id, u.login, u.name, u.team_id, u.role, u.client_id, u.email, t.name as team_name, t.currency, t.lang, t.decimal_mark, t.date_format, t.time_format, t.week_start, t.tracking_mode, t.project_required, t.task_required, t.record_type, t.uncompleted_indicators, - t.bcc_email, t.plugins, t.config, t.lock_spec, t.workday_hours, t.custom_logo + t.bcc_email, t.plugins, t.config, t.lock_spec, t.workday_minutes, t.custom_logo FROM tt_users u LEFT JOIN tt_teams t ON (u.team_id = t.id) WHERE "; if ($id) $sql .= "u.id = $id"; @@ -106,7 +106,7 @@ class ttUser { $this->currency = $val['currency']; $this->plugins = $val['plugins']; $this->lock_spec = $val['lock_spec']; - $this->workday_hours = $val['workday_hours']; + $this->workday_minutes = $val['workday_minutes']; $this->custom_logo = $val['custom_logo']; // Set user config options. diff --git a/WEB-INF/resources/it.lang.php b/WEB-INF/resources/it.lang.php index a084c763..04b63d88 100644 --- a/WEB-INF/resources/it.lang.php +++ b/WEB-INF/resources/it.lang.php @@ -115,7 +115,7 @@ $i18n_key_words = array( 'button.reset_password' => 'Reset password', 'button.send' => 'Invia', 'button.send_by_email' => 'Invia tramite e-mail', -'button.create_team' => 'Crea team', +'button.create_team' => 'Crea gruppo', 'button.export' => 'Esporta gruppo', 'button.import' => 'Importa gruppo', 'button.close' => 'Chiudi', @@ -417,7 +417,7 @@ $i18n_key_words = array( 'form.import.success' => 'Importazione eseguita con successo.', // Teams form. See example at https://timetracker.anuko.com/admin_teams.php (login as admin first). -'form.teams.hint' => 'Crea un nuovo gruppo creando un account gruppo manager.
Puoi anche importare i dati di un team da un file xml esportato da un altro server Anuko Time Tracker (non sono ammessi login duplicati).', +'form.teams.hint' => 'Crea un nuovo gruppo creando un account gruppo manager.
Puoi anche importare i dati di un gruppo da un file xml esportato da un altro server Anuko Time Tracker (non sono ammessi login duplicati).', // Profile form. See example at https://timetracker.anuko.com/profile_edit.php. 'form.profile.12_hours' => '12 ore', diff --git a/WEB-INF/templates/footer.tpl b/WEB-INF/templates/footer.tpl index eb794461..f3e02b5c 100644 --- a/WEB-INF/templates/footer.tpl +++ b/WEB-INF/templates/footer.tpl @@ -12,7 +12,7 @@
- - - + +
 Anuko Time Tracker 1.17.15.3982 | Copyright © Anuko | +  Anuko Time Tracker 1.17.15.3983 | Copyright © Anuko | {$i18n.footer.credits} | {$i18n.footer.license} | {$i18n.footer.improve} diff --git a/dbinstall.php b/dbinstall.php index 58467bc1..dd360115 100755 --- a/dbinstall.php +++ b/dbinstall.php @@ -709,7 +709,7 @@ if ($_POST) { setChange("ALTER TABLE `tt_log` ADD `paid` tinyint(4) NULL default '0' AFTER `billable`"); } - if ($_POST["convert11400to11714"]) { + if ($_POST["convert11400to11715"]) { setChange("ALTER TABLE `tt_teams` DROP `address`"); setChange("ALTER TABLE `tt_fav_reports` ADD `report_spec` text default NULL AFTER `user_id`"); setChange("ALTER TABLE `tt_fav_reports` ADD `paid_status` tinyint(4) default NULL AFTER `invoice`"); @@ -720,6 +720,10 @@ if ($_POST) { setChange("ALTER TABLE `tt_teams` ADD `config` text default NULL AFTER `custom_logo`"); setChange("ALTER TABLE `tt_monthly_quotas` ADD `minutes` int(11) DEFAULT NULL"); setChange("ALTER TABLE `tt_teams` ADD `workday_minutes` smallint(4) DEFAULT '480' AFTER `workday_hours`"); + setChange("UPDATE `tt_teams` SET `workday_minutes` = 60 * `workday_hours`"); + setChange("ALTER TABLE `tt_teams` DROP `workday_hours`"); + setChange("UPDATE `tt_monthly_quotas` SET `minutes` = 60 * `quota`"); + setChange("ALTER TABLE `tt_monthly_quotas` DROP `quota`"); } if ($_POST["cleanup"]) { @@ -764,7 +768,7 @@ if ($_POST) {

DB Install

-
Create database structure (v1.17.14) + Create database structure (v1.17.15)
(applies only to new installations, do not execute when updating)
@@ -800,8 +804,8 @@ if ($_POST) {

Update database structure (v1.14 to v1.17.14)
Update database structure (v1.14 to v1.17.15)
diff --git a/mysql.sql b/mysql.sql index dc07a1dd..d7711260 100644 --- a/mysql.sql +++ b/mysql.sql @@ -376,7 +376,6 @@ CREATE TABLE `tt_monthly_quotas` ( `team_id` int(11) NOT NULL, # team id `year` smallint(5) UNSIGNED NOT NULL, # quota year `month` tinyint(3) UNSIGNED NOT NULL, # quota month - `quota` decimal(5,2) NOT NULL, # number of work hours in specified month and year `minutes` int(11) DEFAULT NULL, # quota in minutes in specified month and year PRIMARY KEY (`team_id`,`year`,`month`) ); diff --git a/plugins/MonthlyQuota.class.php b/plugins/MonthlyQuota.class.php index 6e420f91..a4be6ee3 100644 --- a/plugins/MonthlyQuota.class.php +++ b/plugins/MonthlyQuota.class.php @@ -34,7 +34,6 @@ class MonthlyQuota { var $db; // Database connection. var $team_id; // Team id. - // Old style constructors are DEPRECATED in PHP 7.0, and will be removed in a future version. You should always use __construct() in new code. function __construct() { $this->db = getConnection(); global $user; @@ -42,13 +41,12 @@ class MonthlyQuota { } // update - deletes a quota, then inserts a new one. - public function update($year, $month, $quota) { + public function update($year, $month, $minutes) { $team_id = $this->team_id; $deleteSql = "DELETE FROM tt_monthly_quotas WHERE year = $year AND month = $month AND team_id = $team_id"; $this->db->exec($deleteSql); - if ($quota){ - $float_quota = $this->quotaToFloat($quota); - $insertSql = "INSERT INTO tt_monthly_quotas (team_id, year, month, quota) values ($team_id, $year, $month, $float_quota)"; + if ($minutes){ + $insertSql = "INSERT INTO tt_monthly_quotas (team_id, year, month, minutes) values ($team_id, $year, $month, $minutes)"; $affected = $this->db->exec($insertSql); return (!is_a($affected, 'PEAR_Error')); } @@ -67,7 +65,7 @@ class MonthlyQuota { // getSingle - obtains a quota for a single month. private function getSingle($year, $month) { $team_id = $this->team_id; - $sql = "SELECT quota FROM tt_monthly_quotas WHERE year = $year AND month = $month AND team_id = $team_id"; + $sql = "SELECT minutes FROM tt_monthly_quotas WHERE year = $year AND month = $month AND team_id = $team_id"; $reader = $this->db->query($sql); if (is_a($reader, 'PEAR_Error')) { return false; @@ -75,19 +73,18 @@ class MonthlyQuota { $row = $reader->fetchRow(); if ($row) - return $row['quota']; + return $row['minutes']; // If we did not find a record, return a calculated monthly quota. $numWorkdays = $this->getNumWorkdays($month, $year); global $user; - return $numWorkdays * $user->workday_hours; // TODO: fix a rounding issue for small values like 0:01 - // Possibly with a database field type change (minutes?). + return $numWorkdays * $user->workday_minutes; } // getMany - returns an array of quotas for a given year for team. private function getMany($year){ $team_id = $this->team_id; - $sql = "SELECT month, quota FROM tt_monthly_quotas WHERE year = $year AND team_id = $team_id"; + $sql = "SELECT month, minutes FROM tt_monthly_quotas WHERE year = $year AND team_id = $team_id"; $result = array(); $res = $this->db->query($sql); if (is_a($res, 'PEAR_Error')) { @@ -95,7 +92,7 @@ class MonthlyQuota { } while ($val = $res->fetchRow()) { - $result[$val['month']] = $val['quota']; + $result[$val['month']] = $val['minutes']; } return $result; diff --git a/quotas.php b/quotas.php index d3e7912e..15a7a49a 100644 --- a/quotas.php +++ b/quotas.php @@ -71,12 +71,12 @@ $quota = new MonthlyQuota(); if ($request->isPost()){ // Validate user input. - if (!ttTimeHelper::isValidDuration($request->getParameter('workdayHours'))) + if (false === ttTimeHelper::durationToMinutes($request->getParameter('workdayHours'))) $err->add($i18n->getKey('error.field'), $i18n->getKey('form.quota.workday_hours')); for ($i = 0; $i < count($months); $i++){ $val = $request->getParameter($months[$i]); - if (!$quota->isValidQuota($val)) + if (false === ttTimeHelper::quotaToMinutes($val)) $err->add($i18n->getKey('error.field'), $months[$i]); } // Finished validating user input. @@ -84,16 +84,17 @@ if ($request->isPost()){ if ($err->no()) { // Handle workday hours. - $hours = $quota->quotaToFloat($request->getParameter('workdayHours')); - if ($hours != $user->workday_hours) { - if (!ttTeamHelper::update($user->team_id, array('name'=>$user->team,'workday_hours'=>$hours))) + $workday_minutes = ttTimeHelper::durationToMinutes($request->getParameter('workdayHours')); + if ($workday_minutes != $user->workday_minutes) { + if (!ttTeamHelper::update($user->team_id, array('name'=>$user->team,'workday_minutes'=>$workday_minutes))) $err->add($i18n->getKey('error.db')); } // Handle monthly quotas for a selected year. $selectedYear = (int) $request->getParameter('year'); for ($i = 0; $i < count($months); $i++){ - if (!$quota->update($selectedYear, $i+1, $request->getParameter($months[$i]))) + $quota_in_minutes = ttTimeHelper::quotaToMinutes($request->getParameter($months[$i])); + if (!$quota->update($selectedYear, $i+1, $quota_in_minutes)) $err->add($i18n->getKey('error.db')); } @@ -107,7 +108,7 @@ if ($request->isPost()){ // Get monthly quotas for the entire year. $monthsData = $quota->get($selectedYear); -$workdayHours = ttTimeHelper::toAbsDuration($user->workday_hours * 60, true); +$workdayHours = ttTimeHelper::toAbsDuration($user->workday_minutes, true); $form = new Form('monthlyQuotasForm'); $form->addInput(array('type'=>'text', 'name'=>'workdayHours', 'value'=>$workdayHours, 'style'=>'width:60px')); @@ -116,7 +117,7 @@ for ($i=0; $i < count($months); $i++) { $value = ""; if (array_key_exists($i+1, $monthsData)){ $value = $monthsData[$i+1]; - $value = ttTimeHelper::toAbsDuration($value * 60, true); + $value = ttTimeHelper::toAbsDuration($value, true); } $name = $months[$i]; $form->addInput(array('type'=>'text','name'=>$name,'maxlength'=>6,'value'=> $value,'style'=>'width:70px')); diff --git a/time.php b/time.php index 25cee96c..27d3cdbe 100644 --- a/time.php +++ b/time.php @@ -66,9 +66,9 @@ if ($user->isPluginEnabled('cf')) { if ($user->isPluginEnabled('mq')){ require_once('plugins/MonthlyQuota.class.php'); $quota = new MonthlyQuota(); - $month_quota = $quota->get($selected_date->mYear, $selected_date->mMonth); + $month_quota_minutes = $quota->get($selected_date->mYear, $selected_date->mMonth); $month_total = ttTimeHelper::getTimeForMonth($user->getActiveUser(), $selected_date); - $minutes_left = round(60*$month_quota) - ttTimeHelper::toMinutes($month_total); + $minutes_left = $month_quota_minutes - ttTimeHelper::toMinutes($month_total); $smarty->assign('month_total', $month_total); $smarty->assign('over_quota', $minutes_left < 0); -- 2.20.1