added fixed-rate and hourly-rate for customer, project and activity (#304)

This commit is contained in:
Kevin Papst
2018-09-06 23:37:25 +02:00
committed by GitHub
parent 6163f33abf
commit 8cae5e8b3f
32 changed files with 1173 additions and 154 deletions

View File

@@ -168,6 +168,8 @@ class KimaiImporterCommand extends Command
$activities = null;
$records = null;
$activityToProject = null;
$fixedRates = null;
$rates = null;
$bytesStart = memory_get_usage(true);
@@ -219,6 +221,22 @@ class KimaiImporterCommand extends Command
return;
}
try {
$fixedRates = $this->fetchAllFromImport('fixedRates');
} catch (\Exception $ex) {
$io->error('Failed to load fixedRates: ' . $ex->getMessage());
return;
}
try {
$rates = $this->fetchAllFromImport('rates');
} catch (\Exception $ex) {
$io->error('Failed to load rates: ' . $ex->getMessage());
return;
}
$bytesCached = memory_get_usage(true);
$io->success('Fetched Kimai v1 data, trying to import now ...');
@@ -246,7 +264,7 @@ class KimaiImporterCommand extends Command
}
try {
$counter = $this->importProjects($io, $projects);
$counter = $this->importProjects($io, $projects, $fixedRates, $rates);
$allImports += $counter;
$io->success('Imported projects: ' . $counter);
} catch (\Exception $ex) {
@@ -256,7 +274,7 @@ class KimaiImporterCommand extends Command
}
try {
$counter = $this->importActivities($io, $activities, $activityToProject);
$counter = $this->importActivities($io, $activities, $activityToProject, $fixedRates, $rates);
$allImports += $counter;
$io->success('Imported activities: ' . $counter);
} catch (\Exception $ex) {
@@ -266,7 +284,7 @@ class KimaiImporterCommand extends Command
}
try {
$counter = $this->importTimesheetRecords($io, $records);
$counter = $this->importTimesheetRecords($io, $records, $fixedRates, $rates);
$allImports += $counter;
$io->success('Imported timesheet records: ' . $counter);
} catch (\Exception $ex) {
@@ -275,10 +293,6 @@ class KimaiImporterCommand extends Command
return;
}
// TODO support fixedRates (projectID, activityID, rate)
// TODO support rates (userID, projectID, activityID, rate)
// TODO dump yaml config from configuration (adminmail, currency_name, date_format_0, language, roundPrecision)
// TODO support preferences (ui.lang, timezone)
// TODO support expenses - new database required
$bytesImported = memory_get_usage(true);
@@ -294,8 +308,8 @@ class KimaiImporterCommand extends Command
}
/**
* Checks if the ghiven database connection for import has an underlying database with a compatible structure.
* This is checked againstto the Kimai version and database revision.
* Checks if the given database connection for import has an underlying database with a compatible structure.
* This is checked against the Kimai version and database revision.
*
* @param SymfonyStyle $io
* @param $requiredVersion
@@ -508,8 +522,8 @@ class KimaiImporterCommand extends Command
* ["phone"]=> NULL
* ["fax"]=> NULL
* ["mobile"]=> NULL
* --- ["mail"]=> NULL
* --- ["homepage"]=> NULL
* ["mail"]=> NULL
* ["homepage"]=> NULL
* ["trash"]=> string(1) "0"
* ["timezone"]=> string(13) "Europe/Berlin"
*
@@ -538,7 +552,9 @@ class KimaiImporterCommand extends Command
->setComment($oldCustomer['comment'])
->setCompany($oldCustomer['company'])
->setFax($oldCustomer['fax'])
->setHomepage($oldCustomer['homepage'])
->setMobile($oldCustomer['mobile'])
->setMail($oldCustomer['mail'])
->setPhone($oldCustomer['phone'])
->setContact($oldCustomer['contact'])
->setAddress($oldCustomer['street'] . PHP_EOL . $oldCustomer['zipcode'] . ' ' . $oldCustomer['city'])
@@ -586,10 +602,12 @@ class KimaiImporterCommand extends Command
*
* @param SymfonyStyle $io
* @param array $projects
* @param array $fixedRates
* @param array $rates
* @return int
* @throws \Exception
*/
protected function importProjects(SymfonyStyle $io, $projects)
protected function importProjects(SymfonyStyle $io, $projects, array $fixedRates, array $rates)
{
$counter = 0;
$entityManager = $this->getDoctrine()->getManager();
@@ -611,6 +629,24 @@ class KimaiImporterCommand extends Command
->setVisible($isActive)
;
foreach ($fixedRates as $fixedRow) {
if ($fixedRow['activityID'] !== null || $fixedRow['projectID'] === null) {
continue;
}
if ($fixedRow['projectID'] == $oldProject['projectID']) {
$project->setFixedRate($fixedRow['rate']);
}
}
foreach ($rates as $ratesRow) {
if ($ratesRow['userID'] !== null || $ratesRow['activityID'] !== null || $ratesRow['projectID'] === null) {
continue;
}
if ($ratesRow['projectID'] == $oldProject['projectID']) {
$project->setHourlyRate($ratesRow['rate']);
}
}
if (!$this->validateImport($io, $project)) {
throw new \Exception('Failed to validate project: ' . $project->getName());
}
@@ -654,10 +690,12 @@ class KimaiImporterCommand extends Command
* @param SymfonyStyle $io
* @param array $activities
* @param array $activityToProject
* @param array $fixedRates
* @param array $rates
* @return int
* @throws \Exception
*/
protected function importActivities(SymfonyStyle $io, array $activities, array $activityToProject)
protected function importActivities(SymfonyStyle $io, array $activities, array $activityToProject, array $fixedRates, array $rates)
{
$counter = 0;
$entityManager = $this->getDoctrine()->getManager();
@@ -680,7 +718,7 @@ class KimaiImporterCommand extends Command
$project = $this->projects[$projectId];
$this->unassignedActivities[$oldActivity['activityID']] = $oldActivity;
$this->createActivity($io, $entityManager, $project, $oldActivity);
$this->createActivity($io, $entityManager, $project, $oldActivity, $fixedRates, $rates);
++$counter;
} else {
$this->unassignedActivities[$oldActivity['activityID']] = $oldActivity;
@@ -695,6 +733,8 @@ class KimaiImporterCommand extends Command
* @param ObjectManager $entityManager
* @param Project $project
* @param array $oldActivity
* @param array $fixedRates
* @param array $rates
* @return Activity
* @throws \Exception
*/
@@ -702,7 +742,9 @@ class KimaiImporterCommand extends Command
SymfonyStyle $io,
ObjectManager $entityManager,
Project $project,
array $oldActivity
array $oldActivity,
array $fixedRates,
array $rates
) {
$activityId = $oldActivity['activityID'];
if (isset($this->activities[$activityId][$project->getId()])) {
@@ -724,6 +766,33 @@ class KimaiImporterCommand extends Command
->setProject($project)
;
foreach ($fixedRates as $fixedRow) {
if ($fixedRow['activityID'] === null) {
continue;
}
if ($fixedRow['projectID'] !== null && $fixedRow['projectID'] !== $project->getId()) {
continue;
}
if ($fixedRow['activityID'] == $oldActivity['activityID']) {
$activity->setFixedRate($fixedRow['rate']);
}
}
foreach ($rates as $ratesRow) {
if ($ratesRow['userID'] !== null || $ratesRow['activityID'] === null) {
continue;
}
if ($ratesRow['projectID'] !== null && $ratesRow['projectID'] !== $project->getId()) {
continue;
}
if ($ratesRow['activityID'] == $oldActivity['activityID']) {
$activity->setHourlyRate($ratesRow['rate']);
}
}
if (!$this->validateImport($io, $activity)) {
throw new \Exception('Failed to validate activity: ' . $activity->getName());
}
@@ -772,14 +841,19 @@ class KimaiImporterCommand extends Command
*
* @param SymfonyStyle $io
* @param array $records
* @param array $fixedRates
* @param array $rates
* @return int
* @throws \Exception
*/
protected function importTimesheetRecords(SymfonyStyle $io, array $records)
protected function importTimesheetRecords(SymfonyStyle $io, array $records, array $fixedRates, array $rates)
{
$counter = 0;
$activityCounter = 0;
$entityManager = $this->getDoctrine()->getManager();
$total = count($records);
$io->writeln('Importing timesheets, please wait');
foreach ($records as $oldRecord) {
$activity = null;
@@ -800,7 +874,7 @@ class KimaiImporterCommand extends Command
if (null === $activity && isset($this->unassignedActivities[$activityId])) {
$oldActivity = $this->unassignedActivities[$activityId];
$activity = $this->createActivity($io, $entityManager, $project, $oldActivity);
$activity = $this->createActivity($io, $entityManager, $project, $oldActivity, $fixedRates, $rates);
++$activityCounter;
}
@@ -811,14 +885,26 @@ class KimaiImporterCommand extends Command
$duration = $oldRecord['end'] - $oldRecord['start'];
$rate = $oldRecord['fixedRate'];
if ((empty($rate) || 0.00 == $rate) && !empty($oldRecord['rate'])) {
$hourlyRate = (float) $oldRecord['rate'];
$rate = (float) $hourlyRate * ($duration / 3600);
$rate = round($rate, 2);
$timesheet = new Timesheet();
$fixedRate = $oldRecord['fixedRate'];
if (!empty($fixedRate) && 0.00 != $fixedRate) {
$timesheet->setFixedRate($fixedRate);
}
$hourlyRate = $oldRecord['rate'];
if (!empty($hourlyRate) && 0.00 != $hourlyRate) {
$timesheet->setHourlyRate($hourlyRate);
}
if ($timesheet->getFixedRate() !== null) {
$timesheet->setRate($timesheet->getFixedRate());
} elseif ($timesheet->getHourlyRate() !== null) {
$rate = $timesheet->getHourlyRate();
$rate = (float) $hourlyRate * ($duration / 3600);
$timesheet->setRate(round($rate, 2));
}
$timesheet = new Timesheet();
$timesheet
->setDescription($oldRecord['description'] ?: ($oldRecord['comment'] ?: null))
->setUser($this->users[$oldRecord['userID']])
@@ -826,7 +912,6 @@ class KimaiImporterCommand extends Command
->setEnd(new \DateTime('@' . $oldRecord['end']))
->setDuration($duration)
->setActivity($activity)
->setRate($rate)
;
if (!$this->validateImport($io, $timesheet)) {
@@ -845,13 +930,20 @@ class KimaiImporterCommand extends Command
$io->error('Reason: ' . $ex->getMessage());
}
if (0 == $counter % 500) {
$io->writeln('Imported ' . $counter . ' timesheet records, import ongoing ...');
$io->write('.');
if (0 == $counter % 80) {
$io->writeln(' ('.$counter.'/'.$total.')');
$entityManager->clear(Timesheet::class);
}
}
for ($i = 0; $i < 80-($counter%80); $i++) {
$io->write(' ');
}
$io->writeln(' ('.$counter.'/'.$total.')');
if ($activityCounter > 0) {
$io->success('Created new (previously unattached) activities during timesheet import: ' . $activityCounter);
$io->success('Created new (previously global) activities during timesheet import: ' . $activityCounter);
}
return $counter;

View File

@@ -10,7 +10,7 @@
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Translation\DataCollectorTranslator;
/**
* The abstract base controller.
@@ -27,29 +27,13 @@ abstract class AbstractController extends Controller
public const ROLE_ADMIN = 'ROLE_ADMIN';
/**
* @return object|\Symfony\Component\Translation\DataCollectorTranslator|\Symfony\Component\Translation\IdentityTranslator
* @return DataCollectorTranslator
*/
protected function getTranslator()
private function getTranslator()
{
return $this->container->get('translator');
}
/**
* A translated helper for denyAccessUnlessGranted()
*
* @param mixed $attributes
* @param mixed $subject
* @param string $translation
* @param array $parameter
* @throws AccessDeniedException
*/
protected function denyUnlessGranted($attributes, $subject = null, $translation = 'access.denied', $parameter = [])
{
$error = $this->getTranslator()->trans($translation, $parameter, self::DOMAIN_ERROR);
// TODO try & catch and add to audit log?
$this->denyAccessUnlessGranted($attributes, $subject, $error);
}
/**
* Adds a "successful" flash message to the stack.
*
@@ -72,7 +56,7 @@ abstract class AbstractController extends Controller
/**
* Adds a "warning" flash message to the stack.
*
* @param $translationKey
* @param string $translationKey
* @param array $parameter
*/
protected function flashWarning($translationKey, $parameter = [])
@@ -91,7 +75,7 @@ abstract class AbstractController extends Controller
/**
* Adds a "error" flash message to the stack.
*
* @param $translationKey
* @param string $translationKey
* @param array $parameter
*/
protected function flashError($translationKey, $parameter = [])

View File

@@ -13,6 +13,7 @@ use App\Entity\Activity;
use App\Repository\ActivityRepository;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
/**
* Controller used to manage activity contents in the public part of the site.
@@ -32,7 +33,7 @@ class ActivityController extends Controller
/**
* The flyout to render recent activities and quick-start new recordings.
*
* @return \Symfony\Component\HttpFoundation\Response
* @return Response
*/
public function recentActivitiesAction()
{

View File

@@ -70,7 +70,7 @@ class ProfileController extends AbstractController
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]);
return $this->redirectToRoute('user_profile_edit', ['username' => $profile->getUsername()]);
}
return $this->getProfileView($profile, 'settings', $form);
@@ -95,7 +95,7 @@ class ProfileController extends AbstractController
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]);
return $this->redirectToRoute('user_profile_password', ['username' => $profile->getUsername()]);
}
return $this->getProfileView($profile, 'password', null, $form);
@@ -120,7 +120,7 @@ class ProfileController extends AbstractController
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]);
return $this->redirectToRoute('user_profile_api_token', ['username' => $profile->getUsername()]);
}
return $this->getProfileView($profile, 'api-token', null, null, null, null, $form);
@@ -142,7 +142,7 @@ class ProfileController extends AbstractController
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]);
return $this->redirectToRoute('user_profile_roles', ['username' => $profile->getUsername()]);
}
return $this->getProfileView($profile, 'roles', null, null, $form);
@@ -186,7 +186,7 @@ class ProfileController extends AbstractController
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('user_profile', ['username' => $profile->getUsername()]);
return $this->redirectToRoute('user_profile_preferences', ['username' => $profile->getUsername()]);
}
return $this->getProfileView($profile, 'preferences', null, null, null, $form);

View File

@@ -99,7 +99,7 @@ class TimesheetController extends AbstractController
}
/**
* The route to stop a running entry.
* The route to start a running entry.
*
* @Route(path="/start/{id}", name="timesheet_start", requirements={"id" = "\d+"}, methods={"GET", "POST"})
* @Security("is_granted('start', activity)")

View File

@@ -9,6 +9,8 @@
namespace App\Doctrine;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\DBAL\DBALException;
use Doctrine\Migrations\AbstractMigration as BaseAbstractMigration;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -50,7 +52,7 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai
/**
* @return string
* @throws \Doctrine\DBAL\DBALException
* @throws DBALException
*/
protected function getPlatform()
{
@@ -62,7 +64,7 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai
* $schema = $this->getClassMetaData(User::class);
*
* @param string $entityName
* @return \Doctrine\Common\Persistence\Mapping\ClassMetadata
* @return ClassMetadata
*/
protected function getClassMetaData($entityName)
{
@@ -77,7 +79,7 @@ abstract class AbstractMigration extends BaseAbstractMigration implements Contai
*
* @param string $indexName
* @param string $tableName
* @throws \Doctrine\DBAL\DBALException
* @throws DBALException
*/
protected function addSqlDropIndex($indexName, $tableName)
{

View File

@@ -69,6 +69,22 @@ class Activity
*/
private $timesheets;
/**
* @var float
*
* @ORM\Column(name="fixed_rate", type="decimal", precision=10, scale=2, nullable=true)
* @Assert\GreaterThanOrEqual(0)
*/
private $fixedRate = null;
/**
* @var float
*
* @ORM\Column(name="hourly_rate", type="decimal", precision=10, scale=2, nullable=true)
* @Assert\GreaterThanOrEqual(0)
*/
private $hourlyRate = null;
/**
* @return Timesheet[]
*/
@@ -176,6 +192,44 @@ class Activity
return $this->id;
}
/**
* @return float
*/
public function getFixedRate(): ?float
{
return $this->fixedRate;
}
/**
* @param float $fixedRate
* @return Activity
*/
public function setFixedRate(?float $fixedRate)
{
$this->fixedRate = $fixedRate;
return $this;
}
/**
* @return float
*/
public function getHourlyRate(): ?float
{
return $this->hourlyRate;
}
/**
* @param float $hourlyRate
* @return Activity
*/
public function setHourlyRate(?float $hourlyRate)
{
$this->hourlyRate = $hourlyRate;
return $this;
}
/**
* @return string
*/

View File

@@ -149,6 +149,22 @@ class Customer
*/
private $timezone;
/**
* @var float
*
* @ORM\Column(name="fixed_rate", type="decimal", precision=10, scale=2, nullable=true)
* @Assert\GreaterThanOrEqual(0)
*/
private $fixedRate = null;
/**
* @var float
*
* @ORM\Column(name="hourly_rate", type="decimal", precision=10, scale=2, nullable=true)
* @Assert\GreaterThanOrEqual(0)
*/
private $hourlyRate = null;
/**
* @return int
*/
@@ -509,6 +525,44 @@ class Customer
return $this->projects;
}
/**
* @return float
*/
public function getFixedRate(): ?float
{
return $this->fixedRate;
}
/**
* @param float $fixedRate
* @return Customer
*/
public function setFixedRate(?float $fixedRate)
{
$this->fixedRate = $fixedRate;
return $this;
}
/**
* @return float
*/
public function getHourlyRate(): ?float
{
return $this->hourlyRate;
}
/**
* @param float $hourlyRate
* @return Customer
*/
public function setHourlyRate(?float $hourlyRate)
{
$this->hourlyRate = $hourlyRate;
return $this;
}
/**
* @return string
*/

View File

@@ -85,6 +85,22 @@ class Project
*/
private $activities;
/**
* @var float
*
* @ORM\Column(name="fixed_rate", type="decimal", precision=10, scale=2, nullable=true)
* @Assert\GreaterThanOrEqual(0)
*/
private $fixedRate = null;
/**
* @var float
*
* @ORM\Column(name="hourly_rate", type="decimal", precision=10, scale=2, nullable=true)
* @Assert\GreaterThanOrEqual(0)
*/
private $hourlyRate = null;
/**
* Get projectid
*
@@ -244,6 +260,44 @@ class Project
return $this;
}
/**
* @return float
*/
public function getFixedRate(): ?float
{
return $this->fixedRate;
}
/**
* @param float $fixedRate
* @return Project
*/
public function setFixedRate(?float $fixedRate)
{
$this->fixedRate = $fixedRate;
return $this;
}
/**
* @return float
*/
public function getHourlyRate(): ?float
{
return $this->hourlyRate;
}
/**
* @param float $hourlyRate
* @return Project
*/
public function setHourlyRate(?float $hourlyRate)
{
$this->hourlyRate = $hourlyRate;
return $this;
}
/**
* @return string
*/

View File

@@ -291,6 +291,7 @@ class Timesheet
public function setFixedRate(?float $fixedRate)
{
$this->fixedRate = $fixedRate;
return $this;
}
@@ -309,6 +310,7 @@ class Timesheet
public function setHourlyRate(?float $hourlyRate)
{
$this->hourlyRate = $hourlyRate;
return $this;
}

View File

@@ -15,6 +15,7 @@ use App\Form\Type\YesNoType;
use App\Repository\ProjectRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
@@ -55,6 +56,14 @@ class ActivityEditForm extends AbstractType
return $repo->builderForEntityType($project);
},
])
->add('fixedRate', NumberType::class, [
'label' => 'label.fixed_rate',
'required' => false,
])
->add('hourlyRate', NumberType::class, [
'label' => 'label.hourly_rate',
'required' => false,
])
// boolean
->add('visible', YesNoType::class, [
'label' => 'label.visible',

View File

@@ -15,6 +15,7 @@ use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CountryType;
use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TelType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
@@ -45,9 +46,6 @@ class CustomerEditForm extends AbstractType
'label' => 'label.comment',
'required' => false,
])
->add('visible', YesNoType::class, [
'label' => 'label.visible',
])
->add('company', TextType::class, [
'label' => 'label.company',
'required' => false,
@@ -89,6 +87,17 @@ class CustomerEditForm extends AbstractType
->add('timezone', TimezoneType::class, [
'label' => 'label.timezone',
])
->add('fixedRate', NumberType::class, [
'label' => 'label.fixed_rate',
'required' => false,
])
->add('hourlyRate', NumberType::class, [
'label' => 'label.hourly_rate',
'required' => false,
])
->add('visible', YesNoType::class, [
'label' => 'label.visible',
])
;
}

View File

@@ -17,6 +17,7 @@ use App\Repository\CustomerRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
@@ -58,14 +59,22 @@ class ProjectEditForm extends AbstractType
return $repo->builderForEntityType($customer);
},
])
->add('visible', YesNoType::class, [
'label' => 'label.visible',
->add('fixedRate', NumberType::class, [
'label' => 'label.fixed_rate',
'required' => false,
])
->add('hourlyRate', NumberType::class, [
'label' => 'label.hourly_rate',
'required' => false,
])
->add('budget', MoneyType::class, [
'label' => 'label.budget',
'currency' => $customer ? $customer->getCurrency() : $builder->getOption('currency'),
'required' => false,
])
->add('visible', YesNoType::class, [
'label' => 'label.visible',
])
;
if ($entry->getId() === null) {

View File

@@ -107,7 +107,7 @@ final class Version20180715160326 extends AbstractMigration
$this->addSql('INSERT INTO ' . $users . ' (id, name, mail, active, password, roles, alias, registration_date, title, avatar) SELECT id, username, email, enabled, password, roles, alias, registration_date, title, avatar FROM __temp__' . $users);
$this->addSql('DROP TABLE __temp__' . $users);
} else {
$this->addSql('ALTER TABLE ' . $users . ' CHANGE username name VARCHAR(60) NOT NULL COLLATE utf8mb4_unicode_ci, CHANGE email mail VARCHAR(160) NOT NULL COLLATE utf8mb4_unicode_ci, DROP username_canonical, DROP email_canonical, DROP salt, DROP last_login, DROP confirmation_token, DROP password_requested_at, CHANGE password password VARCHAR(254) DEFAULT \'NULL\' COLLATE utf8mb4_unicode_ci, CHANGE roles roles JSON NOT NULL COLLATE utf8mb4_bin COMMENT \'(DC2Type:json_array)\', CHANGE alias alias VARCHAR(60) DEFAULT \'NULL\' COLLATE utf8mb4_unicode_ci, CHANGE registration_date registration_date DATETIME DEFAULT \'NULL\', CHANGE title title VARCHAR(50) DEFAULT \'NULL\' COLLATE utf8mb4_unicode_ci, CHANGE avatar avatar VARCHAR(255) DEFAULT \'NULL\' COLLATE utf8mb4_unicode_ci, CHANGE enabled active TINYINT(1) NOT NULL');
$this->addSql('ALTER TABLE ' . $users . ' CHANGE username name VARCHAR(60) NOT NULL COLLATE utf8mb4_unicode_ci, CHANGE email mail VARCHAR(160) NOT NULL COLLATE utf8mb4_unicode_ci, DROP username_canonical, DROP email_canonical, DROP salt, DROP last_login, DROP confirmation_token, DROP password_requested_at, CHANGE password password VARCHAR(254) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE roles roles JSON NOT NULL COLLATE utf8mb4_bin COMMENT \'(DC2Type:json_array)\', CHANGE alias alias VARCHAR(60) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE registration_date registration_date DATETIME DEFAULT NULL, CHANGE title title VARCHAR(50) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE avatar avatar VARCHAR(255) DEFAULT NULL COLLATE utf8mb4_unicode_ci, CHANGE enabled active TINYINT(1) NOT NULL');
}
$this->addSql('UPDATE ' . $users . ' SET roles = \'["ROLE_SUPER_ADMIN"]\' WHERE roles LIKE "%ROLE_SUPER_ADMIN%"');

View File

@@ -1,4 +1,13 @@
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace DoctrineMigrations;
@@ -10,7 +19,7 @@ use Doctrine\DBAL\Schema\Schema;
*/
final class Version20180903202256 extends AbstractMigration
{
public function up(Schema $schema) : void
public function up(Schema $schema): void
{
$platform = $this->getPlatform();
@@ -19,25 +28,12 @@ final class Version20180903202256 extends AbstractMigration
}
$timesheet = $this->getTableName('timesheet');
$user = $this->getTableName('users');
$activity = $this->getTableName('activities');
if ($platform === 'sqlite') {
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, start_time, end_time, duration, description, rate FROM ' . $timesheet);
$this->addSql('DROP TABLE ' . $timesheet);
$this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER DEFAULT NULL, activity_id INTEGER DEFAULT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL COLLATE BINARY, rate NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL, CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_4F60C6B181C06096 FOREIGN KEY (activity_id) REFERENCES ' . $activity . ' (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE)');
$this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate) SELECT id, user, activity_id, start_time, end_time, duration, description, rate, null, null FROM __temp__' . $timesheet);
$this->addSql('DROP TABLE __temp__' . $timesheet);
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
} else {
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649');
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD fixed_rate NUMERIC(10, 2) DEFAULT NULL, ADD hourly_rate NUMERIC(10, 2) DEFAULT NULL');
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id) ON DELETE CASCADE');
}
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD COLUMN fixed_rate NUMERIC(10, 2) DEFAULT NULL');
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD COLUMN hourly_rate NUMERIC(10, 2) DEFAULT NULL');
}
public function down(Schema $schema) : void
public function down(Schema $schema): void
{
$platform = $this->getPlatform();
@@ -46,7 +42,6 @@ final class Version20180903202256 extends AbstractMigration
}
$timesheet = $this->getTableName('timesheet');
$user = $this->getTableName('users');
if ($platform === 'sqlite') {
$this->addSql('DROP INDEX IDX_4F60C6B18D93D649');
@@ -59,9 +54,8 @@ final class Version20180903202256 extends AbstractMigration
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
} else {
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP FOREIGN KEY FK_4F60C6B18D93D649');
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP fixed_rate, DROP hourly_rate');
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD CONSTRAINT FK_4F60C6B18D93D649 FOREIGN KEY (user) REFERENCES ' . $user . ' (id)');
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP hourly_rate');
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP fixed_rate');
}
}
}

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace DoctrineMigrations;
use App\Doctrine\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Adding hourly_rate and fixed_rate to:
* - Activities
* - Projects
* - Customer
*/
final class Version20180905190737 extends AbstractMigration
{
public function up(Schema $schema): void
{
$platform = $this->getPlatform();
if (!in_array($platform, ['sqlite', 'mysql'])) {
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
}
$activity = $this->getTableName('activities');
$project = $this->getTableName('projects');
$customer = $this->getTableName('customers');
$this->addSql('ALTER TABLE ' . $activity . ' ADD COLUMN fixed_rate NUMERIC(10, 2) DEFAULT NULL');
$this->addSql('ALTER TABLE ' . $activity . ' ADD COLUMN hourly_rate NUMERIC(10, 2) DEFAULT NULL');
$this->addSql('ALTER TABLE ' . $project . ' ADD COLUMN fixed_rate NUMERIC(10, 2) DEFAULT NULL');
$this->addSql('ALTER TABLE ' . $project . ' ADD COLUMN hourly_rate NUMERIC(10, 2) DEFAULT NULL');
$this->addSql('ALTER TABLE ' . $customer . ' ADD COLUMN fixed_rate NUMERIC(10, 2) DEFAULT NULL');
$this->addSql('ALTER TABLE ' . $customer . ' ADD COLUMN hourly_rate NUMERIC(10, 2) DEFAULT NULL');
}
public function down(Schema $schema): void
{
$platform = $this->getPlatform();
if (!in_array($platform, ['sqlite', 'mysql'])) {
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
}
$activity = $this->getTableName('activities');
$project = $this->getTableName('projects');
$customer = $this->getTableName('customers');
if ($platform === 'sqlite') {
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $customer . ' AS SELECT id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, mail, homepage, timezone FROM ' . $customer);
$this->addSql('DROP TABLE ' . $customer);
$this->addSql('CREATE TABLE ' . $customer . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name VARCHAR(255) NOT NULL, number VARCHAR(50) DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, company VARCHAR(255) DEFAULT NULL, contact VARCHAR(255) DEFAULT NULL, address CLOB DEFAULT NULL, country VARCHAR(2) NOT NULL, currency VARCHAR(3) NOT NULL, phone VARCHAR(255) DEFAULT NULL, fax VARCHAR(255) DEFAULT NULL, mobile VARCHAR(255) DEFAULT NULL, mail VARCHAR(255) DEFAULT NULL, homepage VARCHAR(255) DEFAULT NULL, timezone VARCHAR(255) NOT NULL)');
$this->addSql('INSERT INTO ' . $customer . ' (id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, mail, homepage, timezone) SELECT id, name, number, comment, visible, company, contact, address, country, currency, phone, fax, mobile, mail, homepage, timezone FROM __temp__' . $customer);
$this->addSql('DROP TABLE __temp__' . $customer);
$this->addSql('DROP INDEX IDX_407F12069395C3F3');
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $project . ' AS SELECT id, customer_id, name, order_number, comment, visible, budget FROM ' . $project);
$this->addSql('DROP TABLE ' . $project);
$this->addSql('CREATE TABLE ' . $project . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, customer_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, order_number CLOB DEFAULT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL, budget NUMERIC(10, 2) NOT NULL)');
$this->addSql('INSERT INTO ' . $project . ' (id, customer_id, name, order_number, comment, visible, budget) SELECT id, customer_id, name, order_number, comment, visible, budget FROM __temp__' . $project);
$this->addSql('DROP TABLE __temp__' . $project);
$this->addSql('CREATE INDEX IDX_407F12069395C3F3 ON ' . $project . ' (customer_id)');
$this->addSql('DROP INDEX IDX_8811FE1C166D1F9C');
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $activity . ' AS SELECT id, project_id, name, comment, visible FROM ' . $activity);
$this->addSql('DROP TABLE ' . $activity);
$this->addSql('CREATE TABLE ' . $activity . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, project_id INTEGER DEFAULT NULL, name VARCHAR(255) NOT NULL, comment CLOB DEFAULT NULL, visible BOOLEAN NOT NULL)');
$this->addSql('INSERT INTO ' . $activity . ' (id, project_id, name, comment, visible) SELECT id, project_id, name, comment, visible FROM __temp__' . $activity);
$this->addSql('DROP TABLE __temp__' . $activity);
$this->addSql('CREATE INDEX IDX_8811FE1C166D1F9C ON ' . $activity . ' (project_id)');
} else {
$this->addSql('ALTER TABLE ' . $customer . ' DROP hourly_rate');
$this->addSql('ALTER TABLE ' . $customer . ' DROP fixed_rate');
$this->addSql('ALTER TABLE ' . $project . ' DROP hourly_rate');
$this->addSql('ALTER TABLE ' . $project . ' DROP fixed_rate');
$this->addSql('ALTER TABLE ' . $activity . ' DROP hourly_rate');
$this->addSql('ALTER TABLE ' . $activity . ' DROP fixed_rate');
}
}
}

View File

@@ -41,15 +41,79 @@ class RateCalculator implements CalculatorInterface
return;
}
if (null !== $record->getFixedRate()) {
$record->setRate($record->getFixedRate());
$fixedRate = $this->findFixedRate($record);
if (null !== $fixedRate) {
$record->setRate($fixedRate);
return;
}
$rate = $this->calculateRate($record);
$hourlyRate = $this->findHourlyRate($record);
$factor = $this->getRateFactor($record);
$record->setRate($rate * $factor);
$record->setRate(
$this->calculateRate($record->getDuration(), $hourlyRate, $factor)
);
}
/**
* @param Timesheet $record
* @return float
*/
protected function findHourlyRate(Timesheet $record)
{
if (null !== $record->getHourlyRate()) {
return $record->getHourlyRate();
}
$activity = $record->getActivity();
if (null !== $activity->getHourlyRate()) {
return $activity->getHourlyRate();
}
$project = $activity->getProject();
if (null !== $project) {
if (null !== $project->getHourlyRate()) {
return $project->getHourlyRate();
}
$customer = $project->getCustomer();
if (null !== $customer->getHourlyRate()) {
return $customer->getHourlyRate();
}
}
return (float) $record->getUser()->getPreferenceValue(UserPreference::HOURLY_RATE, 0);
}
/**
* @param Timesheet $record
* @return float|null
*/
protected function findFixedRate(Timesheet $record)
{
if (null !== $record->getFixedRate()) {
return $record->getFixedRate();
}
$activity = $record->getActivity();
if (null !== $activity->getFixedRate()) {
return $activity->getFixedRate();
}
$project = $activity->getProject();
if (null !== $project) {
if (null !== $project->getFixedRate()) {
return $project->getFixedRate();
}
$customer = $project->getCustomer();
if (null !== $customer->getFixedRate()) {
return $customer->getFixedRate();
}
}
return null;
}
/**
@@ -80,17 +144,13 @@ class RateCalculator implements CalculatorInterface
}
/**
* @param Timesheet $record
* @param int $duration
* @param float $hourlyRate
* @param float $factor
* @return float
*/
protected function calculateRate(Timesheet $record)
protected function calculateRate($duration, $hourlyRate, $factor)
{
if (null !== $record->getHourlyRate()) {
$hourlyRate = $record->getHourlyRate();
} else {
$hourlyRate = (float)$record->getUser()->getPreferenceValue(UserPreference::HOURLY_RATE, 0);
}
return (float) $hourlyRate * ($record->getDuration() / 3600);
return (float) $hourlyRate * ($duration / 3600) * $factor;
}
}