From 4e9dbf72a384692c27178286d9a94c9022e74f52 Mon Sep 17 00:00:00 2001 From: Toby Batch Date: Mon, 20 May 2019 16:10:38 +0100 Subject: [PATCH] Initial commit --- Command/NeontribeCvsImportCommand.php | 229 ++++++++++++++++++ .../NeontribeCvsImportExtension.php | 19 ++ Entity/NeontribeCvsImport.php | 29 +++ LICENSE | 21 ++ NeontribeCvsImportBundle.php | 15 ++ README.md | 41 ++++ Repository/NeontribeCvsImportRepository.php | 52 ++++ Resources/config/services.yaml | 18 ++ composer.json | 28 +++ 9 files changed, 452 insertions(+) create mode 100644 Command/NeontribeCvsImportCommand.php create mode 100644 DependencyInjection/NeontribeCvsImportExtension.php create mode 100644 Entity/NeontribeCvsImport.php create mode 100644 LICENSE create mode 100644 NeontribeCvsImportBundle.php create mode 100644 README.md create mode 100644 Repository/NeontribeCvsImportRepository.php create mode 100644 Resources/config/services.yaml create mode 100644 composer.json diff --git a/Command/NeontribeCvsImportCommand.php b/Command/NeontribeCvsImportCommand.php new file mode 100644 index 0000000..ee626e9 --- /dev/null +++ b/Command/NeontribeCvsImportCommand.php @@ -0,0 +1,229 @@ +doctrine = $registry; + $this->repository = $repository; + $this->configuration = $configuration; + + parent::__construct(self::$defaultName); + } + + /** + * + * {@inheritdoc} + */ + protected function configure() { + $this->setName('kimai:csv:import') + ->setDescription('Import from CSV') + ->setHelp('Read timesheets from a CSV file and import records. Creates Customers, Projects and Activities as needed.') + ->addArgument('file', InputArgument::REQUIRED, 'Relative path to the CSV file.') + ->addOption('offset', null, InputArgument::OPTIONAL, 'Number of rows to skip before starting the import', 0) + ->addOption('count', null, InputArgument::OPTIONAL, 'Number of rows to import', 99999) + ->addOption('ignore-hashes', null, InputArgument::OPTIONAL, 'Ignore hash clashes and always create a new timesheet', 1); + } + + /** + * + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) { + $io = new SymfonyStyle($input, $output); + + $filename = $input->getArgument('file'); + $offset = $input->getOption('offset'); + $count = $input->getOption('count'); + $ignoreHashes = $input->getOption('count'); + + $file = new \SplFileObject($filename); + if ($offset > 0) { + $file->seek($offset); + } + + $hashData = $this->repository->getHashData(); + $hashes = array_keys($hashData); + + for ($i = 0; $i < $count and $file->valid(); $i ++, $file->next()) { + $line = str_getcsv($file->current()); + if (count($line) != 9) { + $io->error("Wrong count (" . count($line) . ") in line " . ($offset + $i) . ": " . $file->current()); + continue; + } + $hash = md5($file->current()); + if (! $ignoreHashes && in_array($hash, $hashes)) { + $io->comment("Duplicate timesheet, skipping line " . ($offset + $i) . ", ID " . $line[0]); + continue; + } + + $this->importLine($line); + $hashData[$hash] = $line[0]; + } + + $this->doctrine->getManager()->flush(); + $this->repository->saveHashData($hashData); + } + + public function importLine($elements) { + $customerName = $elements[1]; + $projectName = $elements[2]; + $activityName = $elements[3]; + $start = $elements[4]; + $duration = $elements[5]; + $description = $elements[6]; + $userName = $elements[7]; + $email = $elements[8]; + + $customer = $this->getCompany($customerName); + $project = $this->getProject($projectName, $customer); + $activity = $this->getActivity($activityName, $project); + $user = $this->getUser($userName, $email); + + $begin = new \DateTime($start); + $end = new \DateTime($start); + $end->add(new \DateInterval('PT' . $duration . 'S')); + + $timesheet = new Timesheet(); + $timesheet->setBegin($begin) + ->setEnd($end) + ->setDuration($duration) + ->setDescription($description) + ->setProject($project) + ->setActivity($activity) + ->setUser($user); + + $this->doctrine->getManager()->persist($timesheet); + } + + public function getCompany($customerName): Customer { + $customer = $this->doctrine->getRepository(Customer::class)->findOneBy([ + 'name' => $customerName + ]); + + if ($customer) { + return $customer; + } + + $_customer = new Customer(); + $_customer->setName($customerName) + ->setCountry($this->configuration->find('customer.country')) + ->setCurrency($this->configuration->find('customer.currency')) + ->setTimezone($this->configuration->find('customer.timezone')); + + $entityManager = $this->doctrine->getManager(); + $entityManager->persist($_customer); + $entityManager->flush(); + + return $_customer; + } + + public function getProject($projectName, $customer): Project { + $project = $this->doctrine->getRepository(Project::class)->findOneBy([ + 'name' => $projectName + ]); + + if ($project) { + return $project; + } + + $_project = new Project(); + $_project->setName($projectName)->setCustomer($customer); + + $entityManager = $this->doctrine->getManager(); + $entityManager->persist($_project); + $entityManager->flush(); + + return $_project; + } + + public function getActivity($activityName, $project): Activity { + $activity = $this->doctrine->getRepository(Activity::class)->findOneBy([ + 'name' => $activityName, + 'project' => $project + ]); + + if ($activity) { + return $activity; + } + + $_activity = new Activity(); + $_activity->setName($activityName)->setProject($project); + + $entityManager = $this->doctrine->getManager(); + $entityManager->persist($_activity); + $entityManager->flush(); + + return $_activity; + } + + public function getUser($userName, $userEmail) { + try { + $user = $this->doctrine->getRepository(User::class)->findOneBy([ + 'username' => $userName + ]); + + if ($user) { + return $user; + } + } catch (NoResultException $nre) { + // Fail silently. The above usersearch if the user doesn't exist. + // TODO: Fix this. + } + + $_user = new User(); + $_user->setUsername($userName) + ->setEmail($userEmail) + ->setPassword(md5(uniqid())) + ->setEnabled(true) + ->setRoles([ + User::DEFAULT_ROLE + ]); + + $entityManager = $this->doctrine->getManager(); + $entityManager->persist($_user); + $entityManager->flush(); + + return $_user; + } +} diff --git a/DependencyInjection/NeontribeCvsImportExtension.php b/DependencyInjection/NeontribeCvsImportExtension.php new file mode 100644 index 0000000..9e18a86 --- /dev/null +++ b/DependencyInjection/NeontribeCvsImportExtension.php @@ -0,0 +1,19 @@ +load('services.yaml'); + } catch (\Exception $e) { + echo '[NeontribeCvsImport] invalid services config found: ' . $e->getMessage(); + } + } +} diff --git a/Entity/NeontribeCvsImport.php b/Entity/NeontribeCvsImport.php new file mode 100644 index 0000000..72cd8ed --- /dev/null +++ b/Entity/NeontribeCvsImport.php @@ -0,0 +1,29 @@ +hashes; + } + + /** + * + * @param string|null $customCss + * @return NeontribeCvsImport + */ + public function setHashes(string $hashes = null) { + if (null === $hashes) { + $hashes = ''; + } + + $this->hashes = $hashes; + return $this; + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e6d1cc0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Kevin Papst - https://www.kevinpapst.de + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/NeontribeCvsImportBundle.php b/NeontribeCvsImportBundle.php new file mode 100644 index 0000000..2c4f3eb --- /dev/null +++ b/NeontribeCvsImportBundle.php @@ -0,0 +1,15 @@ +hashesFile = $dataDirectory . '/hashes.json'; + } + + /** + * + * @param array $entity + * @return bool + * @throws \Exception + */ + public function saveHashData(array $hashes) { + if (file_exists($this->hashesFile) && ! is_writable($this->hashesFile)) { + throw new \Exception('Hashes file is not writable: ' . $this->hashesFile); + } + + if (false === file_put_contents($this->hashesFile, json_encode($hashes, JSON_PRETTY_PRINT))) { + throw new \Exception('Failed saving hashes rules to file: ' . $this->hashesFile); + } + + return true; + } + + /** + * + * @return array + */ + public function getHashData(): array { + if (file_exists($this->hashesFile)) { + return json_decode(file_get_contents($this->hashesFile), True); + } else { + return []; + } + } +} diff --git a/Resources/config/services.yaml b/Resources/config/services.yaml new file mode 100644 index 0000000..557d16e --- /dev/null +++ b/Resources/config/services.yaml @@ -0,0 +1,18 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + bind: + $pluginDirectory: "%kimai.plugin_dir%" + $dataDirectory: "%kimai.data_dir%" + + KimaiPlugin\NeontribeCvsImportBundle\: + resource: '../../*' + exclude: '../../{Resources}' + + + KimaiPlugin\NeontribeCvsImportBundle\Command\NeontribeCvsImportCommand: + tags: + - { name: 'console.command', command: 'neontribe:csv:import' } + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3caf59d --- /dev/null +++ b/composer.json @@ -0,0 +1,28 @@ +{ + "name": "neontribe/kimai-cvs-import", + "description": "A Kimai-2 plugin, which allows timesheets/customers/projects/activites to be read from a CSV file.", + "homepage": "https://www.neontribe.co.uk", + "type": "kimai-plugin", + "require": { + "kimai/kimai2-composer": "*" + }, + "keywords": [ + "kimai", + "kimai-plugin" + ], + "license": "MIT", + "authors": [ + { + "name": "Toby Batch", + "email": "tobias@neontribe.co.uk", + "homepage": "https://www.neontribe.co.uk" + } + ], + "extra": { + "kimai": { + "require": "0.9", + "version": "1.1", + "name": "NeontribeCvsImportBundle" + } + } +}